Qual è il modo migliore per impostare un registro su zero nell'assembly x86: xor, mov o e?


120

Tutte le seguenti istruzioni fanno la stessa cosa: impostare %eaxa zero. Qual è il modo ottimale (richiede il minor numero di cicli della macchina)?

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax

6
Potresti leggere questo articolo
Michael Petch

Risposte:


222

TL; Riepilogo DR : xor same, sameè la scelta migliore per tutte le CPU . Nessun altro metodo ha alcun vantaggio su di esso e ha almeno qualche vantaggio su qualsiasi altro metodo. È ufficialmente raccomandato da Intel e AMD e da cosa fanno i compilatori. In modalità a 64 bit, usa ancora xor r32, r32, perché la scrittura di un registro a 32 bit azzera il 32 superiore . xor r64, r64è uno spreco di un byte, perché ha bisogno di un prefisso REX.

Ancora peggio, Silvermont riconosce solo la xor r32,r32dimensione dell'operando di rottura, non a 64 bit. Quindi, anche quando un prefisso REX è ancora richiesto perché stai azzerando r8..r15, usa xor r10d,r10d, notxor r10,r10 .

Esempi di interi GP:

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

L'azzeramento di un registro vettoriale di solito è meglio farlo pxor xmm, xmm. Questo è in genere ciò che fa gcc (anche prima dell'uso con le istruzioni FP).

xorps xmm, xmmpuò avere un senso. È un byte più corto di pxor, ma xorpsrichiede la porta di esecuzione 5 su Intel Nehalem, mentre pxorpuò essere eseguito su qualsiasi porta (0/1/5). (La latenza del ritardo di bypass 2c di Nehalem tra intero e FP di solito non è rilevante, perché l'esecuzione fuori ordine può tipicamente nasconderla all'inizio di una nuova catena di dipendenze).

Nelle microarchitetture della famiglia SnB, nessuno dei due tipi di xor azzeramento necessita nemmeno di una porta di esecuzione. Su AMD, e pre-Nehalem P6 / Core2 Intel, xorpse pxorvengono gestiti allo stesso modo (come istruzioni a numeri interi).

L'uso della versione AVX di un'istruzione vettoriale a 128b azzera anche la parte superiore del registro, quindi vpxor xmm, xmm, xmmè una buona scelta per azzerare YMM (AVX1 / AVX2) o ZMM (AVX512) o qualsiasi futura estensione vettoriale. vpxor ymm, ymm, ymmnon richiede byte aggiuntivi per la codifica, tuttavia, e funziona allo stesso modo su Intel, ma più lento su AMD prima di Zen2 (2 uops). L'azzeramento ZMM dell'AVX512 richiederebbe byte extra (per il prefisso EVEX), quindi l'azzeramento XMM o YMM dovrebbe essere preferito.

Esempi 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.

Vedere l' azzeramento di vxorps su AMD Jaguar / Bulldozer / Zen è più veloce con i registri xmm rispetto a ymm? e
qual è il modo più efficiente per cancellare uno o più registri ZMM su Knights Landing?

Semi-correlato: il modo più veloce per impostare il valore __m256 su tutti i bit ONE e
impostare tutti i bit nel registro CPU su 1 copre in modo efficiente anche i registri k0..7maschera AVX512 . SSE / AVX vpcmpeqdsta danneggiando il dep su molti (sebbene abbia ancora bisogno di un uop per scrivere gli 1), ma AVX512 vpternlogdper i reg ZMM non è nemmeno il dep-breaking. All'interno di un ciclo si consideri la copia da un altro registro invece di ricrearne di nuovi con un ALU uop, specialmente con AVX512.

Ma l'azzeramento è economico: l'azzeramento xor di un registro xmm all'interno di un ciclo è solitamente buono come la copia, tranne su alcune CPU AMD (Bulldozer e Zen) che hanno l'eliminazione dei movimenti per i registri vettoriali ma hanno ancora bisogno di un uop ALU per scrivere zeri per xor -zeroing.


La particolarità dell'azzeramento di idiomi come xor su vari Uarc

Alcune CPU riconoscono sub same,samecome un idioma di azzeramento xor, ma tutte le CPU che riconoscono qualsiasi idioma di azzeramento riconosconoxor . Basta usarlo in xormodo da non doverti preoccupare di quale CPU riconosce quale idioma di azzeramento.

xor(essendo un idioma di azzeramento riconosciuto, a differenza di mov reg, 0) ha alcuni vantaggi evidenti e alcuni sottili (elenco riepilogativo, quindi mi dilungherò su quelli):

  • dimensione del codice inferiore a mov reg,0. (Tutte le CPU)
  • evita penalità di registrazione parziale per codice successivo. (Famiglia Intel P6 e famiglia SnB).
  • non utilizza un'unità di esecuzione, risparmiando energia e liberando risorse di esecuzione. (Famiglia Intel SnB)
  • uop più piccolo (nessun dato immediato) lascia spazio nella riga della cache di uop per le istruzioni vicine da prendere in prestito se necessario. (Famiglia Intel SnB).
  • non utilizza le voci nel file di registro fisico . (Almeno Intel SnB-family (e P4), forse anche AMD poiché utilizzano un design PRF simile invece di mantenere lo stato del registro nel ROB come le microarchitetture della famiglia Intel P6.)

La dimensione del codice macchina più piccola (2 byte invece di 5) è sempre un vantaggio: una densità di codice più alta porta a un minor numero di errori nella cache delle istruzioni e a un migliore recupero delle istruzioni e alla potenziale larghezza di banda di decodifica.


Il vantaggio di non utilizzare un'unità di esecuzione per xor su microarchitetture della famiglia Intel SnB è minore, ma consente di risparmiare energia. È più probabile che sia importante su SnB o IvB, che hanno solo 3 porte di esecuzione ALU. Haswell e successivi hanno 4 porte di esecuzione in grado di gestire istruzioni ALU intere, incluse mov r32, imm32, quindi con un perfetto processo decisionale da parte dello scheduler (cosa che non sempre avviene nella pratica), HSW potrebbe comunque sostenere 4 up per clock anche quando tutti hanno bisogno di ALU porte di esecuzione.

Vedere la mia risposta su un'altra domanda sull'azzeramento dei registri per ulteriori dettagli.

Il post sul blog di Bruce Dawson che Michael Petch ha collegato (in un commento alla domanda) sottolinea che xorviene gestito nella fase di ridenominazione del registro senza bisogno di un'unità di esecuzione (zero uop nel dominio non utilizzato), ma ha mancato il fatto che sia ancora un uop nel dominio fuso. Le moderne CPU Intel possono emettere e ritirare 4 uop con dominio fuso per clock. Ecco da dove viene il limite di 4 zeri per orologio. L'aumento della complessità del registro che rinomina l'hardware è solo uno dei motivi per limitare la larghezza del design a 4. (Bruce ha scritto alcuni post sul blog molto eccellenti, come la sua serie su FP math e x87 / SSE / rounding , cosa che faccio io altamente raccomandato).


Sulle CPU della famiglia AMD Bulldozer , mov immediatefunziona sulle stesse porte di esecuzione di interi EX0 / EX1 di xor. mov reg,regpuò funzionare anche su AGU0 / 1, ma è solo per la copia del registro, non per l'impostazione da immediati. Quindi per quanto ne so, su AMD l'unico vantaggio di xorsopra movè la codifica più breve. Potrebbe anche salvare le risorse del registro fisico, ma non ho visto alcun test.


Gli idiomi di azzeramento riconosciuti evitano sanzioni per registri parziali sulle CPU Intel che rinominano i registri parziali separatamente dai registri completi (famiglie P6 e SnB).

xorsi contrassegnare registro come avente le parti superiori azzerati , così xor eax, eax/ inc al/ inc eaxevita la solita penalità parziale registro che pre-IVb CPU ha. Anche senza xor, IvB ha bisogno di un uop di fusione solo quando gli 8 bit ( AH) alti vengono modificati e poi l'intero registro viene letto, e Haswell lo rimuove anche.

Dalla guida microarch di Agner Fog, pagina 98 (sezione Pentium M, a cui fanno riferimento le sezioni successive, incluso SnB):

Il processore riconosce lo XOR di un registro con se stesso impostandolo a zero. Un tag speciale nel registro ricorda che la parte alta del registro è zero in modo che EAX = AL. Questo tag viene ricordato anche in un loop:

    ; 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

(da pg82): Il processore ricorda che i 24 bit superiori di EAX sono zero fintanto che non si ottengono interruzioni, previsioni errate o altri eventi di serializzazione.

pg82 di quella guida conferma anche che nonmov reg, 0 è riconosciuto come un idioma di azzeramento, almeno sui primi progetti P6 come PIII o PM. Sarei molto sorpreso se spendessero i transistor per rilevarlo su CPU successive.


xorimposta i flag , il che significa che devi stare attento quando collaudi le condizioni. Poiché setccpurtroppo è disponibile solo con una destinazione a 8 bit , di solito è necessario fare attenzione a evitare sanzioni di registrazione parziale.

Sarebbe stato bello se x86-64 avesse riproposto uno degli opcode rimossi (come AAM) per un 16/32/64 bit setcc r/m, con il predicato codificato nel campo a 3 bit del registro sorgente del campo r / m (il modo alcune altre istruzioni a singolo operando li usano come bit di codice operativo). Ma non l'hanno fatto, e comunque non sarebbe stato d'aiuto per x86-32.

Idealmente, dovresti usare 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

Questo ha prestazioni ottimali su tutte le CPU (nessuno stallo, unione di uops o false dipendenze).

Le cose sono più complicate quando non vuoi fare xor prima di un'istruzione di impostazione dei flag . ad esempio, vuoi ramificare su una condizione e poi setcc su un'altra condizione dagli stessi flag. ad esempio cmp/jle, setee o non si dispone di un registro di riserva, oppure si desidera xorescludere del tutto il percorso del codice non utilizzato.

Non esistono idiomi di azzeramento riconosciuti che non influenzino i flag, quindi la scelta migliore dipende dalla microarchitettura di destinazione. Su Core2, l'inserimento di un uop di fusione potrebbe causare uno stallo di 2 o 3 cicli. Sembra essere più economico su SnB, ma non ho passato molto tempo a cercare di misurare. L'utilizzo di mov reg, 0/ setccavrebbe una penalità significativa sulle vecchie CPU Intel e sarebbe ancora un po 'peggiore sulle nuove Intel.

L'uso di setcc/ movzx r32, r8è probabilmente la migliore alternativa per le famiglie Intel P6 e SnB, se non è possibile eseguire xor-zero prima dell'istruzione di impostazione dei flag. Dovrebbe essere meglio che ripetere il test dopo un xor azzeramento. (Non considerare nemmeno sahf/ lahfo pushf/ popf). IvB può eliminare movzx r32, r8(cioè gestirlo con la ridenominazione del registro senza unità di esecuzione o latenza, come l'azzeramento xor). Haswell e successivi eliminano solo le movistruzioni regolari , quindi movzxprende un'unità di esecuzione e ha una latenza diversa da zero, rendendo test / setcc/ movzxpeggiore di xor/ test / setcc, ma comunque buono almeno quanto test / mov r,0/ setcc(e molto meglio sulle vecchie CPU).

L'utilizzo di setcc/ movzxsenza azzeramento prima non è corretto su AMD / P4 / Silvermont, perché non tengono traccia dei Dep separatamente per i sub-registri. Ci sarebbe una falsa dipendenza dal vecchio valore del registro. L'uso di mov reg, 0/ setccper l'azzeramento / rottura delle dipendenze è probabilmente la migliore alternativa quando xor/ test / setccnon è un'opzione.

Ovviamente, se non è necessario che setccl'output di sia maggiore di 8 bit, non è necessario azzerare nulla. Tuttavia, fai attenzione alle false dipendenze da CPU diverse da P6 / SnB se scegli un registro che è stato recentemente parte di una lunga catena di dipendenze. (E fai attenzione a non causare un registro parziale o un uop extra se chiami una funzione che potrebbe salvare / ripristinare il registro di cui stai utilizzando una parte.)


andcon uno zero immediato non è un caso speciale in quanto indipendente dal vecchio valore su qualsiasi CPU di cui sono a conoscenza, quindi non interrompe le catene di dipendenza. Non ha vantaggi xore molti svantaggi.

È utile solo per scrivere microbenchmark quando si desidera una dipendenza come parte di un test di latenza, ma si desidera creare un valore noto azzerando e aggiungendo.


Vedi http://agner.org/optimize/ per i dettagli del microarch , incluso quali idiomi di azzeramento sono riconosciuti come interruzione delle dipendenze (ad esempio sub same,sameè su alcune ma non tutte le CPU, mentre xor same,sameè riconosciuto su tutti) movinterrompe la catena di dipendenze dal vecchio valore del registro (indipendentemente dal valore sorgente, zero o meno, perché è così che movfunziona). xorinterrompe le catene di dipendenze solo nel caso speciale in cui src e dest sono lo stesso registro, motivo per cui movviene escluso dalla lista dei separatori di dipendenze appositamente riconosciuti. (Inoltre, perché non è riconosciuto come un idioma di azzeramento, con gli altri vantaggi che comporta.)

È interessante notare che il progetto P6 più vecchio (da PPro a Pentium III) non riconosceva l' xorazzeramento come un interruttore di dipendenza, solo come un idioma di azzeramento allo scopo di evitare stalli di registri parziali , quindi in alcuni casi valeva la pena usarli entrambi mov e poi xor-zero in quell'ordine per rompere il dep e poi di nuovo azzerare + impostare il bit del tag interno che i bit alti siano zero quindi EAX = AX = AL.

Vedi l'esempio 6.17 di Agner Fog. nel suo microarca pdf. Dice che questo vale anche per P2, P3 e persino (presto?) PM. Un commento sul post del blog collegato dice che era solo PPro ad avere questa supervisione, ma ho provato su Katmai PIII e @Fanael ha testato su un Pentium M, ed entrambi abbiamo scoperto che non ha interrotto una dipendenza per una latenza imulcatena legata . Ciò conferma i risultati di Agner Fog, purtroppo.


TL: DR:

Se rende davvero il tuo codice più gradevole o salva le istruzioni, allora certo, zero con movper evitare di toccare i flag, a patto che non introduci un problema di prestazioni diverso dalla dimensione del codice. Evitare flag di clobbering è l'unica ragione ragionevole per non usare xor, ma a volte puoi xor-zero prima della cosa che imposta i flag se hai un registro di riserva.

mov-zero prima di setccè migliore per la latenza rispetto a movzx reg32, reg8dopo (tranne su Intel quando è possibile selezionare registri diversi), ma dimensione del codice peggiore.


7
La maggior parte delle istruzioni aritmetiche OP R, S sono costrette da una CPU fuori servizio ad attendere che il contenuto del registro R sia riempito da istruzioni precedenti con registro R come obiettivo; questa è una dipendenza dai dati. Il punto chiave è che i chip Intel / AMD hanno hardware speciale per rompere le dipendenze di attesa per i dati sul registro R quando si incontra XOR R, R e non lo fa necessariamente per altre istruzioni di azzeramento del registro. Ciò significa che l'istruzione XOR può essere programmata per l'esecuzione immediata, ed è per questo che Intel / AMD consiglia di utilizzarla.
Ira Baxter

3
@IraBaxter: Sì, e solo per evitare confusione (perché ho visto questo malinteso su SO), mov reg, srcinterrompe anche le catene di dep per le CPU OO (indipendentemente dal fatto che src sia imm32 [mem], o un altro registro). Questa rottura delle dipendenze non viene menzionata nei manuali di ottimizzazione perché non è un caso speciale che si verifica solo quando src e dest sono lo stesso registro. E ' sempre accade per le istruzioni che non dipendono dalla loro dest. (ad eccezione dell'implementazione di Intel di popcnt/lzcnt/tzcntavere un falso dep sulla destinazione)
Peter Cordes

2
@ Zboson: La "latenza" di un'istruzione senza dipendenze è importante solo se c'era una bolla nella pipeline. È utile per l'eliminazione dei movimenti, ma per le istruzioni di azzeramento il vantaggio di latenza zero entra in gioco solo dopo qualcosa come un errore di previsione del ramo o un errore, in cui l'esecuzione è in attesa delle istruzioni decodificate, piuttosto che che i dati siano pronti. Ma sì, l'eliminazione dei movimenti non rende movlibera, solo zero latenza. La parte "non prendere una porta di esecuzione" di solito non è importante. La velocità effettiva del dominio fuso può facilmente essere il collo di bottiglia, specialmente. con carichi o depositi nel mix.
Peter Cordes

2
Secondo Agner KNL non riconosce l'indipendenza dei registri a 64 bit. Quindi xor r64, r64non spreca solo un byte. Come dici tu xor r32, r32è la scelta migliore soprattutto con KNL. Vedere la sezione 15.7 "Casi speciali di indipendenza" in questo manuale di micrarch se si desidera saperne di più.
Bosone Z

3
ah, dov'è il buon vecchio MIPS, con il suo "registro zero" quando ne hai bisogno.
hayalci
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.