“Temukan alasan untuk semuanya dan kamu akan mengerti banyak”
Mungkin pembaca reguler saya (yah, mereka tidak mungkin) ingat bahwa dalam posting saya saya bingung bahwa atribut yang tidak ditandatangani digunakan untuk menggambarkan register perangkat eksternal. Dalam komentar, disarankan agar ini dilakukan untuk menghindari perilaku yang tidak jelas selama shift dan saya setuju. Seperti yang baru-baru ini saya temukan, ada alasan lain untuk penggunaan atribut ini dan dapat diterapkan tidak hanya untuk register, tetapi juga untuk variabel biasa.
Jadi, kita mulai.
Sebagai permulaan, pengantar kecil untuk besiSebagai platform target, kami akan mempertimbangkan MK 8-bit tanpa baterai (ini adalah upaya yang menyedihkan untuk menyembunyikan nama AVR yang dikompromikan), yang memiliki perintah yang diimplementasikan perangkat keras berikut:
lsl / lsr logis kiri / kanan, bit rendah / tinggi dihapus;
rol / ror siklik kiri / kanan melalui transfer (shift 9 bit);
asr aritmatika bergeser ke kanan, bit yang paling signifikan (ditandatangani) disimpan (kami memperhatikan fakta bahwa untuk melakukan pergeseran jenis ini ke kiri umumnya tidak mungkin pada prinsipnya).
Semua perintah ini dieksekusi pada operan byte dan merupakan dasar untuk implementasi semua shift lain yang mungkin. Misalnya, pergeseran kata (2 byte rh, rl) dengan tanda di sebelah kanan dengan 1 digit diimplementasikan dengan urutan berikut:
asr rh; ror rl;
Pertimbangkan contoh kode sederhana dan kode assembler yang sesuai untuk MK dengan sistem perintah AVR, seperti biasa, diperoleh di godbolt.org. (menyiratkan bahwa optimasi diaktifkan dan variabelnya terletak di register r24)
int8_t byte; byte = byte << 1;
clr r25 sbrc r24,7 com r25 lsl r24 rol r25
dan melihat bahwa operasinya memakan waktu lima tim?
Catatan: Jika seseorang dalam komentar memberi tahu Anda bagaimana mengatur fragmen ini (dan yang berikutnya) dalam 2 kolom, saya akan berterima kasih.
Hal ini dapat dilihat dari kode assembler bahwa variabel byte diperluas ke tipe integer (16-bit) dalam tiga perintah pertama, dan dalam dua berikutnya, angka byte ganda benar-benar bergeser - entah bagaimana itu aneh, setidaknya aneh.
Bergeser ke kanan tidak lebih baik
byte = byte >> 1; clr r25 sbrc r24,7 com r25 asr r25 ror r24
- lima tim yang sama. Sementara itu, jelas bahwa pada kenyataannya, untuk melakukan operasi terakhir, Anda memerlukan satu perintah tunggal
sr r24
dan untuk operasi pertama tidak ada lagi. Saya telah berulang kali menyatakan bahwa kompiler saat ini membuat kode assembler tidak lebih buruk daripada seorang programmer (meskipun itu adalah sistem perintah ARM), terutama jika Anda membantunya sedikit, dan tiba-tiba gelandangan seperti itu. Tetapi cobalah membantu kompiler untuk membuat kode yang benar, mungkin ini masalah pencampuran tipe dalam operasi shift dan coba
byte = byte >> (int8_t) 1;
- tidak membantu, dari kata "sepenuhnya", tetapi pilihan
byte=(uint8_t) byte >> 1;
memberikan hasil yang sedikit lebih baik
ldi r25,lo8(0) asr r25 ror r24
- tiga tim, karena ekspansi ke keseluruhan sekarang menempati satu tim - lebih baik, meskipun tidak sempurna, gambar yang sama untuk
byte=(uint8_t) byte << 1;
- tiga tim. Nah, agar tidak menulis gips tambahan, kami membuat variabel itu sendiri tanpa tanda
uint8_t byteu;
dan BINGO - kode assembler sepenuhnya memenuhi harapan kami
byteu = byteu << 1; lsr r24
Aneh bagaimana kelihatannya, apa perbedaannya, untuk menunjukkan tipe variabel yang benar dengan segera, atau untuk membawanya langsung ke operasi - tetapi ternyata ada perbedaan.
Studi lebih lanjut menunjukkan bahwa kode assembler memperhitungkan jenis variabel yang hasilnya ditetapkan
byteu = byte << 1;
berfungsi dengan baik dan menghasilkan kode minimal, dan opsi
byte = byteu << 1;
tidak bisa tanpa tiga tim.
Tentunya, perilaku seperti itu dijelaskan dalam standar bahasa, saya bertanya kepada mereka yang tahu dalam komentar, tetapi sekali lagi saya akan dengan bangga menyatakan bahwa "Chukchi bukan pembaca" dan saya akan melanjutkan ceritanya.
Jadi, langkah seperti itu tidak membantu bergeser ke kanan - seperti sebelumnya, ada 3 tim (yah. Yang bukan 5, seperti untuk versi tanda) dan saya tidak bisa meningkatkan hasilnya dengan cara apa pun.
Tetapi bagaimanapun juga, kita melihat bahwa operasi shift dengan nomor yang tidak ditandatangani dilakukan lebih cepat daripada dengan lawannya. Oleh karena itu, jika kita tidak akan memperlakukan bit nomor paling signifikan sebagai tanda (dan dalam kasus register, biasanya demikian), maka kita tentu perlu menambahkan atribut yang tidak ditandatangani, yang akan kita lakukan di masa mendatang.
Ternyata dengan bergeser secara umum, semuanya sangat menarik, mari kita mulai meningkatkan jumlah posisi ketika bergeser ke kiri dan melihat hasilnya: << 1 dibutuhkan 1 siklus clock, << 2 - 2, << 3 - 3, 4 - 2 tanpa diduga, kompiler menerapkan optimasi rumit yang rumit
swap r24 andi r24,lo8(-16)
di mana perintah s swap menukar dua camilan dalam satu byte. Selanjutnya, berdasarkan optimasi terakhir << 5 - 3, << 6 - 4, << 7 - 3 lagi secara tak terduga, ada optimasi lain
ror r24 clr r24 ror r24
bit transfer digunakan, << 8 - 0 mengukur, karena ternyata 0, tidak ada gunanya mencari lebih jauh.
Ngomong-ngomong, ini tugas yang menarik untuk Anda - untuk waktu minimum apa Anda bisa melakukan operasi
uint16_t byteu; byteu = byteu << 4;
yang menerjemahkan 0x1234 ke 0x2340. Solusi yang jelas adalah dengan menjalankan beberapa perintah 4 kali
lsl rl rol rh
mengarah ke 4 * 2 = 8 langkah, saya cepat-cepat datang dengan pilihan
swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 mov tmp,rl andi tmp,0x0f or rh,tmp ; 2343 andi rl,0xf0 ; 2340
yang membutuhkan 7 langkah dan register perantara. Jadi, kompiler menghasilkan kode 6 perintah dan tidak ada register perantara - keren, ya.
Saya menyembunyikan kode ini di bawah spoiler - coba cari solusinya sendiri.Petunjuk: dalam rangkaian perintah MK ada perintah EKSKLUSIF ATAU atau TOTAL JUMLAH DUA
Ini dia, kode yang luar biasa ini swap rl ; 1243 swap rh ; 2143 andi rh,0xf0 ; 2043 eor rh,rl ; 6343 andi r2l,0xf0 ; 6340 eor rh,rl ; 2340
Saya hanya mendapatkan kesenangan estetika dari fragmen ini.
Biasanya, untuk angka 16-bit, perbedaan antara kode untuk nomor yang ditandatangani dan yang tidak ditandatangani menghilang ketika bergeser ke kiri, aneh seperti itu.
Mari kita kembali ke byte kita dan mulai bergerak ke kanan. Seperti yang kita ingat, untuk byte yang ditandatangani kita memiliki 5 siklus clock, untuk byte yang tidak ditandai - 3 dan kali ini tidak dapat dikurangi. Atau semua sama itu mungkin - ya, itu mungkin, tetapi sangat aneh (GCC dengan optimisasi dihidupkan - "ini adalah tempat yang sangat aneh"), yaitu ini
byteu = (byteu >> 1) & 0x7F;
yang menghasilkan tepat satu perintah untuk kedua varian tanda. Cocok dan opsi
byteu = (byteu & 0xFE) >> 1;
tetapi hanya untuk nomor yang tidak ditandatangani, dengan yang sudah ditandatangani semuanya menjadi lebih menyedihkan - 7 langkah, jadi kami terus mengeksplorasi hanya opsi pertama.
Saya tidak bisa mengatakan bahwa saya mengerti apa yang terjadi, karena jelas bahwa perkalian logis (&) dengan konstanta seperti itu setelah perubahan semacam itu tidak masuk akal (dan itu tidak masuk akal), tetapi kehadiran & operasi mempengaruhi kode dari pergeseran itu sendiri. "Kamu melihat gopher - tidak - dan aku tidak melihat, tetapi dia."
Bergeser 2 dan seterusnya menunjukkan bahwa penting untuk melunasi bit tanda, tetapi jumlah awalnya tidak ditandatangani, secara umum, beberapa sampah diperoleh, "tetapi berhasil," adalah satu-satunya hal yang dapat dikatakan tentang ini.
Namun demikian, aman untuk mengatakan bahwa menafsirkan isi register dan memori sebagai angka yang tidak ditandatangani memungkinkan Anda untuk melakukan sejumlah operasi (misalnya, menggeser atau memperluas nilai) dengan mereka lebih cepat dan menghasilkan kode yang lebih kompak, sehingga sangat direkomendasikan untuk menulis program untuk MK, kecuali kalau tidak (penafsiran sebagai angka sudah umum) bukanlah prasyarat.