Mengapa instruksi x86-64 pada register 32-bit membubarkan bagian atas register 64-bit penuh?

119

Dalam Tour Manual Intel x86-64 , saya membaca

Mungkin fakta yang paling mengejutkan adalah bahwa instruksi seperti MOV EAX, EBXsecara otomatis nol di atas 32 bit RAXregister.

Dokumentasi Intel (3.4.1.1 Register Tujuan Umum dalam Mode 64-Bit dalam Arsitektur Dasar manual) yang dikutip dari sumber yang sama memberi tahu kita:

  • Operand 64-bit menghasilkan hasil 64-bit di register tujuan umum.
  • Operand 32-bit menghasilkan hasil 32-bit, hasil perluasan nol ke 64-bit dalam register tujuan umum tujuan.
  • Operand 8-bit dan 16-bit menghasilkan hasil 8-bit atau 16-bit. 56 bit atas atau 48 bit (masing-masing) dari register tujuan umum tujuan tidak dimodifikasi oleh operasi. Jika hasil dari operasi 8-bit atau 16-bit dimaksudkan untuk kalkulasi alamat 64-bit, secara eksplisit tandatangani-perpanjang register ke 64-bit penuh.

Dalam perakitan x86-32 dan x86-64, instruksi 16 bit seperti

mov ax, bx

jangan perlihatkan perilaku "aneh" seperti ini yang kata atas eax di nol.

Jadi: apa alasan mengapa perilaku ini diperkenalkan? Sekilas sepertinya tidak masuk akal (tetapi alasannya mungkin karena saya terbiasa dengan kebiasaan perakitan x86-32).

Nubok
sumber
16
Jika Anda Google untuk "Kios pendaftaran sebagian", Anda akan menemukan cukup banyak informasi tentang masalah yang mereka (hampir pasti) coba hindari.
Jerry Coffin
4
Bukan hanya "paling". AFAIK, semua instruksi dengan r32operan tujuan nol 32 tinggi, bukan penggabungan. Misalnya, beberapa assembler akan mengganti pmovmskb r64, xmmdengan pmovmskb r32, xmm, menyimpan REX, karena versi tujuan 64-bit berperilaku sama. Meskipun bagian Operasi dari manual mencantumkan semua 6 kombinasi dest 32 / 64bit dan sumber 64/128 / 256b secara terpisah, ekstensi nol implisit dari formulir r32 menduplikasi ekstensi nol eksplisit dari formulir r64. Saya ingin tahu tentang implementasi HW ...
Peter Cordes
2
@HansPassant, referensi melingkar dimulai.
kchoi

Jawaban:

98

Saya bukan AMD atau berbicara untuk mereka, tetapi saya akan melakukannya dengan cara yang sama. Karena memusatkan perhatian pada separuh tinggi tidak membuat ketergantungan pada nilai sebelumnya, CPU harus menunggu. The mendaftar mengubah nama Mekanisme akan dasarnya dikalahkan jika hal itu tidak dilakukan dengan cara itu.

Dengan cara ini Anda dapat menulis kode cepat menggunakan nilai 32-bit dalam mode 64-bit tanpa harus secara eksplisit memutuskan dependensi sepanjang waktu. Tanpa perilaku ini, setiap instruksi 32-bit dalam mode 64-bit harus menunggu sesuatu yang terjadi sebelumnya, meskipun bagian yang tinggi itu hampir tidak akan pernah digunakan. (Membuat int64-bit akan membuang jejak cache dan bandwidth memori; x86-64 paling efisien mendukung ukuran operan 32 dan 64-bit )

Perilaku untuk ukuran operan 8 dan 16-bit adalah yang aneh. Kegilaan ketergantungan adalah salah satu alasan mengapa instruksi 16-bit dihindari sekarang. x86-64 mewarisi ini dari 8086 untuk 8-bit dan 386 untuk 16-bit, dan memutuskan untuk memiliki register 8 dan 16-bit bekerja dengan cara yang sama dalam mode 64-bit seperti yang mereka lakukan dalam mode 32-bit.


Lihat juga Mengapa GCC tidak menggunakan register parsial? untuk detail praktis tentang bagaimana penulisan ke register parsial 8 dan 16-bit (dan pembacaan register lengkap selanjutnya) ditangani oleh CPU sebenarnya.

Harold
sumber
8
Saya tidak berpikir itu aneh, saya pikir mereka tidak ingin terlalu banyak merusak dan menyimpan perilaku lama di sana.
Alexey Frunze
5
@Alex ketika mereka memperkenalkan mode 32bit, tidak ada perilaku lama untuk bagian yang tinggi. Tidak ada bagian yang tinggi sebelumnya .. Tentu saja setelah itu tidak bisa diubah lagi.
Harold
1
Saya berbicara tentang operan 16-bit, mengapa bit teratas tidak menjadi nol dalam kasus itu. Mereka tidak dalam mode non-64-bit. Dan itu juga disimpan dalam mode 64-bit.
Alexey Frunze
3
Saya menafsirkan "Perilaku untuk instruksi 16bit adalah yang aneh" sebagai "aneh bahwa ekstensi nol tidak terjadi dengan operan 16-bit dalam mode 64-bit". Karenanya komentar saya tentang menyimpannya dengan cara yang sama dalam mode 64-bit untuk kompatibilitas yang lebih baik.
Alexey Frunze
8
@Alex oh begitu. Baik. Saya tidak berpikir itu aneh dari perspektif itu. Hanya dari perspektif "melihat ke belakang, mungkin itu bukan ide yang bagus". Kira saya seharusnya lebih jelas :)
Harold
9

Ini hanya menghemat ruang dalam instruksi, dan set instruksi. Anda dapat memindahkan nilai kecil langsung ke register 64-bit dengan menggunakan instruksi (32-bit) yang ada.

Ini juga menghindarkan Anda dari keharusan mengenkode nilai 8 byte MOV RAX, 42, ketika MOV EAX, 42dapat digunakan kembali.

Pengoptimalan ini tidak begitu penting untuk operasi 8 dan 16 bit (karena lebih kecil), dan mengubah aturan di sana juga akan merusak kode lama.

Bo Persson
sumber
7
Jika itu benar, bukankah akan lebih masuk akal untuk sign-extended daripada 0 extended?
Damien_The_Unbeliever
16
Ekstensi tanda lebih lambat, bahkan di perangkat keras. Ekstensi nol dapat dilakukan secara paralel dengan perhitungan apa pun yang menghasilkan setengah bagian bawah, tetapi ekstensi tanda tidak dapat dilakukan sampai (setidaknya tanda) bagian bawah telah dihitung.
Jerry Coffin
13
Trik terkait lainnya adalah menggunakan XOR EAX, EAXkarena XOR RAX, RAXakan membutuhkan awalan REX.
Neil
3
@Nubok: Tentu, mereka dapat menambahkan pengkodean movzx / movsx yang membutuhkan argumen langsung. Seringkali akan lebih mudah jika bit atas di-zero-kan, sehingga Anda dapat menggunakan nilai sebagai indeks array (karena semua reg harus berukuran sama di alamat yang efektif: [rsi + edx]tidak diizinkan). Tentu saja menghindari ketergantungan palsu / kios register sebagian (jawaban lain) adalah alasan utama lainnya.
Peter Cordes
4
dan mengubah aturan di sana juga akan merusak kode lama. Kode lama tidak dapat berjalan dalam mode 64-bit (mis. 1-byte inc / dec adalah prefiks REX); ini tidak relevan. Alasan tidak membersihkan kutil dari x86 adalah perbedaan yang lebih sedikit antara mode panjang dan mode compat / legacy, jadi lebih sedikit instruksi yang harus didekode secara berbeda tergantung pada mode. AMD tidak tahu AMD64 akan populer, dan sayangnya sangat konservatif sehingga membutuhkan lebih sedikit transistor untuk mendukung. Dalam jangka panjang, akan baik-baik saja jika kompiler dan manusia harus mengingat hal mana yang bekerja secara berbeda dalam mode 64-bit.
Peter Cordes
1

Tanpa nol yang meluas ke 64 bit, itu berarti pembacaan instruksi dari raxakan memiliki 2 dependensi untuk raxoperannya (instruksi yang menulis ke eaxdan instruksi yang menulis ke raxsebelumnya), ini berarti bahwa 1) ROB harus memiliki entri untuk beberapa dependensi untuk satu operand, yang berarti ROB akan membutuhkan lebih banyak logika dan transistor serta membutuhkan lebih banyak ruang, dan eksekusi akan lebih lambat menunggu dependensi kedua yang tidak perlu yang mungkin membutuhkan waktu lama untuk dieksekusi; atau alternatifnya 2), yang saya duga terjadi dengan instruksi 16 bit, tahap alokasi mungkin terhenti (yaitu jika RAT memiliki alokasi aktif untuk axpenulisan dan eaxpembacaan muncul, berhenti sampai axpenulisan dihentikan).

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

Satu-satunya keuntungan dari bukan zero extending adalah memastikan bit urutan yang lebih tinggi raxdisertakan, misalnya, jika aslinya berisi 0xffffffffffffffff, hasilnya adalah 0xffffffff00000007, tetapi ada sedikit alasan bagi ISA untuk membuat jaminan ini dengan biaya seperti itu, dan kemungkinan besar manfaat ekstensi nol sebenarnya lebih dibutuhkan, jadi ini menghemat baris kode tambahan mov rax, 0. Dengan menjamin itu akan selalu nol diperpanjang hingga 64 bit, kompiler dapat bekerja dengan aksioma ini dalam pikiran sementara di mov rdx, rax, raxhanya harus menunggu ketergantungan tunggal, yang berarti dapat memulai eksekusi lebih cepat dan berhenti, membebaskan unit eksekusi. Selain itu, ini juga memungkinkan idiom nol yang lebih efisien seperti xor eax, eaxnol raxtanpa memerlukan byte REX.

Lewis Kelsey
sumber
Bendera parsial di Skylake setidaknya berfungsi dengan memiliki input terpisah untuk CF vs. SPAZO mana pun. (Jadi cmovbe2 uops tapi cmovb1). Tetapi tidak ada CPU yang melakukan penggantian nama register parsial yang melakukannya seperti yang Anda sarankan. Sebaliknya mereka menyisipkan uop penggabungan jika sebagian reg diganti namanya secara terpisah dari reg penuh (yaitu "kotor"). Lihat Mengapa GCC tidak menggunakan register parsial? dan Bagaimana tepatnya kinerja sebagian register di Haswell / Skylake? Menulis AL tampaknya memiliki ketergantungan yang salah pada RAX, dan AH tidak konsisten
Peter Cordes
CPU keluarga P6 terhenti selama ~ 3 siklus untuk memasukkan gabungan uop (Core2 / Nehalem), atau keluarga P6 sebelumnya (PM, PIII, PII, PPro) hanya berhenti selama (setidaknya?) ~ 6 siklus. Mungkin itu seperti yang Anda sarankan di 2, menunggu nilai reg lengkap tersedia melalui writeback ke file register permanen / arsitektural.
Peter Cordes
@PeterCordes oh, saya tahu tentang menggabungkan Uops setidaknya untuk kios bendera parsial. Masuk akal, tapi saya lupa cara kerjanya selama satu menit; itu diklik sekali tapi saya lupa membuat catatan
Lewis Kelsey
@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXSaya tidak dapat menemukan contoh 'penggabungan uop' yang akan digunakan untuk menyelesaikan masalah ini, sama untuk kios bendera parsial
Lewis Kelsey
Benar, P6 awal hanya berhenti sampai ditulisi kembali. Core2 dan Nehalem menyisipkan uop penggabungan setelah / sebelum? hanya menunda bagian depan untuk waktu yang lebih singkat. Sandybridge memasukkan uops penggabungan tanpa terhenti. (Tapi penggabungan AH harus mengeluarkan siklus dengan sendirinya, sementara penggabungan AL dapat menjadi bagian dari grup penuh.) Haswell / SKL tidak mengganti nama AL secara terpisah dari RAX sama sekali, begitu mov al, [mem]juga dengan beban fusi mikro + ALU- merge, hanya mengganti nama AH, dan UOP penggabungan AH masih bermasalah saja. Mekanisme penggabungan flag parsial dalam CPU ini bervariasi, misalnya Core2 / Nehalem masih berhenti untuk flag parsial, tidak seperti parsial-reg.
Peter Cordes