Apa cara terbaik untuk mengatur register ke nol di perakitan x86: xor, mov atau dan?

Jawaban:

222

Ringkasan TL; DR : xor same, sameadalah pilihan terbaik untuk semua CPU . Tidak ada metode lain yang memiliki keunggulan di atasnya, dan setidaknya memiliki beberapa keunggulan dibandingkan metode lain. Ini secara resmi direkomendasikan oleh Intel dan AMD, dan apa yang dilakukan oleh kompiler. Dalam mode 64-bit, tetap gunakan xor r32, r32, karena penulisan 32-bit reg nol di atas 32 . xor r64, r64adalah pemborosan byte, karena memerlukan awalan REX.

Lebih buruk lagi, Silvermont hanya mengenali xor r32,r32sebagai dep-breaking, bukan 64-bit operand-size. Jadi, meskipun awalan REX masih diperlukan karena Anda memusatkan perhatian pada r8..r15, gunakan xor r10d,r10d, bukanxor r10,r10 .

Contoh GP-integer:

xor   eax, eax       ; RAX = 0.  Including AL=0 etc.
xor   r10d, r10d     ; R10 = 0
xor   edx, edx       ; RDX = 0

; small code-size alternative:    cdq    ; zero RDX if EAX is already zero

; SUB-OPTIMAL
xor   rax,rax       ; waste of a REX prefix, and extra slow on Silvermont
xor   r10,r10       ; bad on Silvermont (not dep breaking), same as r10d everywhere else because a REX prefix is still needed for r10d or r10.
mov   eax, 0        ; doesn't touch FLAGS, but not faster and takes more bytes
 and   eax, 0        ; false dependency.  (Microbenchmark experiments might want this)
 sub   eax, eax      ; same as xor on most but not all CPUs; bad on Silvermont for example.

xor   al, al        ; false dep on some CPUs, not a zeroing idiom.  Use xor eax,eax
mov   al, 0         ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified

Mengosongkan register vektor biasanya paling baik dilakukan dengan pxor xmm, xmm. Biasanya itulah yang dilakukan gcc (bahkan sebelum digunakan dengan instruksi FP).

xorps xmm, xmmbisa masuk akal. Ini satu byte lebih pendek dari pxor, tetapi xorpsmembutuhkan port eksekusi 5 di Intel Nehalem, sementara pxordapat berjalan di port apa pun (0/1/5). (Latensi penundaan bypass 2c Nehalem antara integer dan FP biasanya tidak relevan, karena eksekusi yang tidak sesuai pesanan biasanya dapat menyembunyikannya di awal rantai ketergantungan baru).

Pada mikroarsitektur keluarga-SnB, tidak ada rasa xor-zeroing yang bahkan membutuhkan port eksekusi. Pada AMD, dan pra-Nehalem P6 / Core2 Intel, xorpsdan pxorditangani dengan cara yang sama (seperti instruksi vektor-integer).

Menggunakan versi AVX dari instruksi vektor 128b nol bagian atas dari reg juga, jadi vpxor xmm, xmm, xmmadalah pilihan yang baik untuk memusatkan YMM (AVX1 / AVX2) atau ZMM (AVX512), atau ekstensi vektor masa depan. vpxor ymm, ymm, ymmtidak membutuhkan byte tambahan untuk dikodekan, dan berjalan sama pada Intel, tetapi lebih lambat pada AMD sebelum Zen2 (2 uops). Pengosongan AVX512 ZMM akan membutuhkan byte tambahan (untuk awalan EVEX), jadi pengenolan XMM atau YMM harus lebih disukai.

Contoh XMM / YMM / ZMM

    # Good:
 xorps   xmm0, xmm0         ; smallest code size (for non-AVX)
 pxor    xmm0, xmm0         ; costs an extra byte, runs on any port on Nehalem.
 xorps   xmm15, xmm15       ; Needs a REX prefix but that's unavoidable if you need to use high registers without AVX.  Code-size is the only penalty.

   # Good with AVX:
 vpxor xmm0, xmm0, xmm0    ; zeros X/Y/ZMM0
 vpxor xmm15, xmm0, xmm0   ; zeros X/Y/ZMM15, still only 2-byte VEX prefix

#sub-optimal AVX
 vpxor xmm15, xmm15, xmm15  ; 3-byte VEX prefix because of high source reg
 vpxor ymm0, ymm0, ymm0     ; decodes to 2 uops on AMD before Zen2


    # Good with AVX512
 vpxor  xmm15,  xmm0, xmm0     ; zero ZMM15 using an AVX1-encoded instruction (2-byte VEX prefix).
 vpxord xmm30, xmm30, xmm30    ; EVEX is unavoidable when zeroing zmm16..31, but still prefer XMM or YMM for fewer uops on probable future AMD.  May be worth using only high regs to avoid needing vzeroupper in short functions.
    # Good with AVX512 *without* AVX512VL (e.g. KNL / Xeon Phi)
 vpxord zmm30, zmm30, zmm30    ; Without AVX512VL you have to use a 512-bit instruction.

# sub-optimal with AVX512 (even without AVX512VL)
 vpxord  zmm0, zmm0, zmm0      ; EVEX prefix (4 bytes), and a 512-bit uop.  Use AVX1 vpxor xmm0, xmm0, xmm0 even on KNL to save code size.

Lihat Apakah vxorps-zeroing pada AMD Jaguar / Bulldozer / Zen lebih cepat dengan register xmm daripada ymm? dan
Apa cara paling efisien untuk menghapus satu atau beberapa register ZMM di Knights Landing?

Semi terkait: Cara tercepat untuk menyetel nilai __m256 ke semua SATU bit dan
Setel semua bit dalam register CPU ke 1 secara efisien juga mencakup register k0..7mask AVX512 . SSE / AVX vpcmpeqdmerusak banyak (meskipun masih membutuhkan uop untuk menulis 1s), tetapi AVX512 vpternlogduntuk reg ZMM bahkan tidak merusak. Di dalam loop pertimbangkan untuk menyalin dari register lain daripada membuat ulang dengan ALU uop, terutama dengan AVX512.

Tapi zeroing itu murah: xor-zeroing sebuah xmm reg di dalam loop biasanya sama baiknya dengan menyalin, kecuali pada beberapa CPU AMD (Bulldozer dan Zen) yang memiliki mov-elimination untuk vector regs tetapi masih membutuhkan ALU uop untuk menulis nol untuk xor -zeroing.


Apa yang istimewa tentang memusatkan perhatian pada idiom seperti xor pada berbagai uarches

Beberapa CPU dikenali sub same,samesebagai idiom zeroing xor, tetapi semua CPU yang mengenali idiom zeroing mengenalixor . Cukup gunakan xorsehingga Anda tidak perlu khawatir tentang CPU mana yang mengenali idiom zeroing yang mana.

xor(menjadi idiom zeroing yang diakui, tidak seperti mov reg, 0) memiliki beberapa keuntungan yang jelas dan halus (daftar ringkasan, maka saya akan memperluasnya):

  • ukuran kode lebih kecil dari mov reg,0. (Semua CPU)
  • menghindari hukuman pendaftaran sebagian untuk kode selanjutnya. (Intel P6-family dan SnB-family).
  • tidak menggunakan unit eksekusi, menghemat daya dan membebaskan sumber daya eksekusi. (Keluarga Intel SnB)
  • uop yang lebih kecil (tidak ada data langsung) menyisakan ruang di baris cache uop untuk instruksi terdekat untuk dipinjam jika diperlukan. (Intel SnB-family).
  • tidak menggunakan entri dalam file register fisik . (Intel SnB-family (dan P4) setidaknya, mungkin juga AMD karena mereka menggunakan desain PRF yang serupa alih-alih menyimpan status register di ROB seperti mikroarsitektur Intel P6-family.)

Ukuran kode mesin yang lebih kecil (2 byte, bukan 5) selalu merupakan keuntungan: Kepadatan kode yang lebih tinggi menyebabkan lebih sedikit instruksi-cache yang terlewat, dan pengambilan instruksi yang lebih baik serta berpotensi mendekode bandwidth.


Manfaat tidak menggunakan unit eksekusi untuk xor pada mikroarsitektur keluarga Intel SnB kecil, tetapi menghemat daya. Ini lebih mungkin menjadi masalah pada SnB atau IvB, yang hanya memiliki 3 port eksekusi ALU. Haswell dan yang lebih baru memiliki 4 port eksekusi yang dapat menangani instruksi ALU integer, termasuk mov r32, imm32, jadi dengan pengambilan keputusan yang sempurna oleh penjadwal (yang tidak selalu terjadi dalam praktiknya), HSW masih dapat mempertahankan 4 uops per jam bahkan ketika mereka semua membutuhkan ALU port eksekusi.

Lihat jawaban saya pada pertanyaan lain tentang register nol untuk lebih jelasnya.

Entri blog Bruce Dawson yang ditautkan oleh Michael Petch (dalam komentar pada pertanyaan) menunjukkan bahwa xorditangani pada tahap ganti nama register tanpa memerlukan unit eksekusi (nol uops di domain yang tidak digunakan), tetapi melewatkan fakta bahwa itu masih satu uop di domain gabungan. CPU Intel modern dapat mengeluarkan & menghentikan 4 uops domain gabungan per jam. Dari situlah 4 nol per batas jam berasal. Kompleksitas yang meningkat dari perangkat keras yang mengganti nama register hanyalah salah satu alasan untuk membatasi lebar desain menjadi 4. (Bruce telah menulis beberapa posting blog yang sangat bagus, seperti serialnya tentang matematika FP dan masalah x87 / SSE / pembulatan , yang saya lakukan Sangat disarankan).


Pada CPU AMD Bulldozer-family , mov immediateberjalan pada port eksekusi integer EX0 / EX1 yang sama dengan xor. mov reg,regjuga dapat berjalan di AGU0 / 1, tetapi itu hanya untuk penyalinan register, bukan untuk pengaturan dari segera. Jadi AFAIK, AMD satu-satunya keuntungan untuk xorlebih movadalah encoding lebih pendek. Mungkin juga menghemat sumber daya register fisik, tetapi saya belum melihat tes apa pun.


Idiom zeroing yang diakui menghindari penalti register parsial pada CPU Intel yang mengganti nama register parsial secara terpisah dari register penuh (keluarga P6 & SnB).

xorakan menandai register karena bagian atasnya dikosongkan , jadi xor eax, eax/ inc al/ inc eaxhindari penalti register parsial yang biasa dimiliki CPU pra-IvB. Bahkan tanpa xor, IvB hanya membutuhkan penggabungan uop ketika 8bits ( AH) yang tinggi diubah dan kemudian seluruh register dibaca, dan Haswell bahkan menghapusnya.

Dari panduan mikroarch Agner Fog, hal 98 (bagian Pentium M, direferensikan oleh bagian selanjutnya termasuk SnB):

Prosesor mengenali XOR dari register dengan sendirinya sebagai pengaturan ke nol. Sebuah tag khusus pada register mengingat bahwa bagian register yang tinggi adalah nol sehingga EAX = AL. Tag ini diingat bahkan dalam satu putaran:

    ; Example    7.9. Partial register problem avoided in loop
    xor    eax, eax
    mov    ecx, 100
LL:
    mov    al, [esi]
    mov    [edi], eax    ; No extra uop
    inc    esi
    add    edi, 4
    dec    ecx
    jnz    LL

(dari pg82): Prosesor mengingat bahwa 24 bit atas EAX adalah nol selama Anda tidak mendapatkan interupsi, kesalahan prediksi, atau peristiwa serialisasi lainnya.

pg82 panduan yang juga menegaskan bahwa mov reg, 0ini tidak diakui sebagai idiom zeroing, setidaknya pada P6 awal desain seperti PIII atau PM. Saya akan sangat terkejut jika mereka menghabiskan transistor untuk mendeteksinya di CPU selanjutnya.


xorset flags , yang berarti Anda harus berhati-hati saat menguji kondisi. Karena setccsayangnya hanya tersedia dengan tujuan 8bit , Anda biasanya perlu berhati-hati untuk menghindari penalti pendaftaran sebagian.

Akan lebih baik jika x86-64 menggunakan kembali salah satu opcode yang dihapus (seperti AAM) untuk 16/32/64 bit setcc r/m, dengan predikat yang dikodekan di bidang sumber-register 3-bit dari bidang r / m (cara beberapa instruksi operan tunggal lainnya menggunakannya sebagai bit opcode). Tapi mereka tidak melakukannya, dan itu tidak akan membantu untuk x86-32.

Idealnya, Anda harus menggunakan xor/ set flags / setcc/ read full register:

...
call  some_func
xor     ecx,ecx    ; zero *before* the test
test    eax,eax
setnz   cl         ; cl = (some_func() != 0)
add     ebx, ecx   ; no partial-register penalty here

Ini memiliki kinerja yang optimal pada semua CPU (tidak ada stall, penggabungan uops, atau dependensi palsu).

Hal-hal menjadi lebih rumit ketika Anda tidak ingin xor sebelum instruksi pengaturan bendera . misalnya Anda ingin bercabang pada satu kondisi dan kemudian setcc pada kondisi lain dari flag yang sama. misalnya cmp/jle,, setedan Anda juga tidak memiliki register cadangan, atau Anda ingin xorsama sekali tidak menyertakan jalur kode yang tidak diambil.

Tidak ada idiom zeroing yang dikenali yang tidak memengaruhi flag, jadi pilihan terbaik bergantung pada mikroarsitektur target. Pada Core2, menyisipkan uop penggabungan dapat menyebabkan 2 atau 3 siklus berhenti. Tampaknya lebih murah di SnB, tetapi saya tidak menghabiskan banyak waktu untuk mencoba mengukurnya. Menggunakan mov reg, 0/ setccakan berdampak signifikan pada CPU Intel yang lebih lama, dan masih sedikit lebih buruk pada Intel yang lebih baru.

Menggunakan setcc/ movzx r32, r8mungkin merupakan alternatif terbaik untuk keluarga Intel P6 & SnB, jika Anda tidak dapat xor-zero sebelum instruksi pengaturan bendera. Itu seharusnya lebih baik daripada mengulang tes setelah xor-zeroing. (Jangan pertimbangkan sahf/ lahfatau pushf/ popf). IvB dapat menghilangkan movzx r32, r8(yaitu menanganinya dengan penggantian nama register tanpa unit eksekusi atau latensi, seperti xor-zeroing). Haswell dan yang lebih baru hanya menghilangkan movinstruksi biasa , jadi movzxmembutuhkan unit eksekusi dan memiliki latensi bukan nol, membuat test / setcc/ movzxlebih buruk dari xor/ test / setcc, tetapi masih setidaknya sebagus test / mov r,0/ setcc(dan jauh lebih baik pada CPU lama).

Menggunakan setcc/ movzxtanpa zeroing pertama buruk pada AMD / P4 / Silvermont, karena mereka tidak melacak deps secara terpisah untuk sub-register. Akan ada kesalahan dep pada nilai lama register. Menggunakan mov reg, 0/ setccuntuk zeroing / pemutusan ketergantungan mungkin merupakan alternatif terbaik ketika xor/ test / setccbukan merupakan pilihan.

Tentu saja, jika Anda tidak membutuhkan setcckeluaran yang lebih lebar dari 8 bit, Anda tidak perlu nol apapun. Namun, waspadalah terhadap dependensi palsu pada CPU selain P6 / SnB jika Anda memilih register yang baru-baru ini menjadi bagian dari rantai dependensi yang panjang. (Dan berhati-hatilah dalam menyebabkan regangan parsial atau uop ekstra jika Anda memanggil fungsi yang mungkin menyimpan / memulihkan register yang Anda gunakan.)


anddengan nol langsung tidak bersifat khusus karena tidak bergantung pada nilai lama pada CPU mana pun yang saya ketahui, jadi tidak memutus rantai ketergantungan. Ini tidak memiliki kelebihan xordan kekurangan.

Ini berguna hanya untuk menulis microbenchmark saat Anda menginginkan dependensi sebagai bagian dari uji latensi, tetapi ingin membuat nilai yang diketahui dengan membidik dan menambahkan.


Lihat http://agner.org/optimize/ untuk detail microarch , termasuk idiom zeroing mana yang dikenali sebagai pemutusan ketergantungan (misalnya sub same,samepada beberapa tetapi tidak semua CPU, sementara xor same,samedikenali pada semua.) movMemutus rantai ketergantungan pada nilai lama dari register (terlepas dari nilai sumbernya, nol atau tidak, karena begitulah cara movkerjanya). xorhanya memutus rantai ketergantungan dalam kasus khusus di mana src dan tujuan adalah register yang sama, itulah sebabnya mengapa movditinggalkan dari daftar pemecah ketergantungan yang dikenali secara khusus . (Juga, karena itu tidak dikenali sebagai idiom zeroing, dengan manfaat lain yang dibawanya.)

Menariknya, desain P6 tertua (PPro hingga Pentium III) tidak mengenali xor-zeroing sebagai pemecah ketergantungan, hanya sebagai idiom zeroing untuk tujuan menghindari kios register parsial , jadi dalam beberapa kasus sebaiknya gunakan keduanya mov dan kemudian xor-Zeroing dalam urutan itu untuk memecahkan dep dan kemudian nol lagi + mengatur bit tag internal bahwa bit tinggi adalah nol jadi EAX = AX = AL.

Lihat Contoh Agner Fog 6.17. di microarch pdf-nya. Katanya ini juga berlaku untuk P2, P3, bahkan PM (dini hari?). Sebuah komentar di posting blog tertaut mengatakan bahwa hanya PPro yang memiliki pengawasan ini, tetapi saya telah menguji Katmai PIII, dan @Fanael mengujinya pada Pentium M, dan kami berdua menemukan bahwa itu tidak merusak ketergantungan untuk latensi imulrantai terikat . Sayangnya, ini menegaskan hasil Agner Fog.


TL: DR:

Jika itu benar-benar membuat kode Anda lebih bagus atau menyimpan instruksi, maka pastikan, nol dengan movuntuk menghindari menyentuh bendera, selama Anda tidak memasukkan masalah kinerja selain ukuran kode. Menghindari clobbering flags adalah satu-satunya alasan yang masuk akal untuk tidak menggunakan xor, tetapi terkadang Anda dapat xor-zero sebelum hal yang menetapkan flag jika Anda memiliki register cadangan.

mov-Nol di depan setcclebih baik untuk latensi daripada movzx reg32, reg8setelahnya (kecuali pada Intel ketika Anda dapat memilih register yang berbeda), tetapi ukuran kode lebih buruk.

Peter Cordes
sumber
7
Kebanyakan instruksi aritmatika OP R, S dipaksa oleh CPU yang rusak untuk menunggu isi register R diisi oleh instruksi sebelumnya dengan register R sebagai target; ini adalah ketergantungan data. Poin kuncinya adalah bahwa chip Intel / AMD memiliki perangkat keras khusus untuk memecahkan ketergantungan data yang harus ditunggu pada register R ketika XOR R, R ditemukan, dan tidak harus melakukannya untuk instruksi zeroing register lainnya. Ini berarti instruksi XOR dapat dijadwalkan untuk eksekusi segera, dan inilah mengapa Intel / AMD merekomendasikan untuk menggunakannya.
Ira Baxter
3
@IraBaxter: Ya, dan hanya untuk menghindari kebingungan (karena saya telah melihat kesalahpahaman ini di SO), mov reg, srcjuga memutus rantai dep untuk OO CPU (terlepas dari src menjadi imm32 [mem], atau register lain). Pemutusan ketergantungan ini tidak disebutkan dalam manual pengoptimalan karena ini bukan kasus khusus yang hanya terjadi jika src dan tujuan adalah register yang sama. Itu selalu terjadi untuk instruksi yang tidak bergantung pada tujuan mereka. (kecuali untuk implementasi Intel popcnt/lzcnt/tzcntmemiliki dep palsu pada tujuan.)
Peter Cordes
2
@ Zboson: "Latensi" dari instruksi tanpa ketergantungan hanya penting jika ada gelembung di pipeline. Ini bagus untuk mov-elimination, tetapi untuk instruksi zeroing, manfaat zero-latency hanya berlaku setelah sesuatu seperti salah prediksi cabang atau I $ miss, di mana eksekusi menunggu instruksi yang diterjemahkan, daripada data siap. Tapi ya, mov-elimination tidak membuat movgratis, hanya latensi nol. Bagian "tidak mengambil port eksekusi" biasanya tidak penting. Throughput domain-fusi dapat dengan mudah menjadi penghambat, khususnya. dengan beban atau penyimpanan dalam campuran.
Peter Cordes
2
Menurut Agner KNL tidak mengakui Kemandirian register 64-bit. Jadi xor r64, r64tidak hanya menyia-nyiakan satu byte. Seperti yang Anda katakan xor r32, r32adalah pilihan terbaik terutama dengan KNL. Lihat bagian 15.7 "Kasus-kasus khusus kemerdekaan" dalam manual micrarch ini jika Anda ingin membaca lebih lanjut.
Z boson
3
ah, di mana MIPS lama yang bagus , dengan "register nol" saat Anda membutuhkannya.
hayalci