Verificare se un registro è zero con reg di CMP, 0 vs OR reg, reg?

Esiste una differenza di velocità di esecuzione usando il seguente codice:

cmp al, 0 je done 

e il seguente:

 or al, al jz done 

So che le istruzioni JE e JZ sono le stesse, e anche che usando OR si ottiene un miglioramento della dimensione di un byte. Tuttavia, mi occupo anche della velocità del codice. Sembra che gli operatori logici saranno più veloci di un SUB o di un CMP, ma volevo solo assicurarmi. Questo potrebbe essere un compromesso tra dimensione e velocità, o un vantaggio reciproco (ovviamente il codice sarà più opaco).

Dipende dalla sequenza esatta del codice, dalla CPU specifica e da altri fattori.

Il problema principale con or al, al, è che “modifica” EAX , il che significa che un’istruzione successiva che utilizza EAX in qualche modo potrebbe bloccarsi fino al completamento di questa istruzione. Si noti che il ramo condizionale ( jz ) dipende anche dall’istruzione, ma i produttori di CPU fanno molto lavoro (previsione delle branche e esecuzione speculativa) per attenuarli. Si noti inoltre che in teoria sarebbe ansible per un produttore di CPU progettare una CPU che riconosce che l’ EAX non è cambiato in questo caso specifico, ma ci sono centinaia di questi casi speciali ed i vantaggi di riconoscere la maggior parte di essi sono troppo piccoli.

Il problema principale con cmp al,0 è che è leggermente più grande, il che potrebbe significare un recupero delle istruzioni più lento / più pressione cache e (se si tratta di un ciclo) potrebbe significare che il codice non si adatta più al “loop buffer” della CPU.

Come ha sottolineato Jester nei commenti; test al,al evita entrambi i problemi: è più piccolo di cmp al,0 e non modifica EAX .

Ovviamente (a seconda della sequenza specifica) il valore in AL deve venire da qualche parte e, se proviene da un’istruzione che imposta i flag in modo appropriato, potrebbe essere ansible modificare il codice per evitare di utilizzare un’altra istruzione per impostare nuovamente i flag in un secondo momento.

, c’è una differenza nelle prestazioni.

La scelta migliore per confrontare un registro con zero su x86 moderno è test reg, reg (se ZF non è già impostato in modo appropriato dall’istruzione che ha impostato reg ). È come AND reg,reg ma senza scrivere la destinazione.

or reg,reg non può fare il macro-fuse, aggiunge latenza per tutto ciò che lo legge più tardi, e ha bisogno di un nuovo registro fisico per contenere il risultato. (Quindi utilizza le risorse di ridenominazione dei registri in cui il test non lo farebbe, limitando la finestra delle istruzioni out-of-order della CPU ). (Riscrivere il dst può essere una vittoria su Intel P6-family, però, vedi sotto).


I risultati della bandiera di test reg,reg / and reg,reg / or reg,reg sono identici a cmp reg, 0 in tutti i casi (eccetto per AF):

  • CF = OF = 0 perché test / and fanno sempre, e per cmp perché sottrarre lo zero non può overflow o carry.
  • ZF , SF , PF impostati in base al risultato (es. reg ): reg&reg per test o reg - 0 per cmp. Quindi puoi testare per interi con segno negativo o senza segno con il bit alto impostato guardando SF.

    O con jl , perché OF = 0 quindi la condizione l ( SF!=OF ) è equivalente a SF . Ogni CPU in grado di eseguire la macro-fusione di TEST / JL può anche eseguire il macro-fuse TEST / JS, anche Core2. Ma dopo il CMP byte [mem],0 , usa sempre JL non JS per diramarsi sul bit del segno.

( AF non è definito dopo il test , ma è impostato in base al risultato per cmp . Lo ignoro perché è davvero oscuro: gli unici utenti per AF sono le istruzioni ASCII regolate con lahf BCD come AAS e lahf / pushf .)


test è più breve da codificare di cmp con 0 immediato, in tutti i casi tranne il caso speciale cmp al, imm8 che è ancora due byte. Anche in questo caso, il test è preferibile per ragioni di macro-fusione (con jle e simili su Core2), e poiché non avere affatto alcuna possibilità può aiutare la densità della cache di uop lasciando uno slot che un’altra istruzione può prendere in prestito se ha bisogno di più spazio (SnB -famiglia).


I decodificatori delle CPU Intel e AMD possono eseguire il test macro-fusion e cmp internamente con alcune istruzioni di diramazione condizionale in un’unica operazione di confronto e suddivisione. Questo ti dà un throughput massimo di 5 istruzioni per ciclo quando avviene la fusione macro, contro 4 senza fusione macro. (Per CPU Intel dal Core2.)

Le recenti CPU Intel possono fondere alcune istruzioni (come and e add / sub ) come pure test e cmp , ma or non sono una di queste. Le CPU AMD possono unire solo test e cmp con un JCC. Vedere x86_64 – Assemblaggio – condizioni del loop e fuori servizio , o semplicemente fare riferimento direttamente ai documenti del microarch di Agner Fog per i dettagli di cui la CPU può fare il macro-fusibile. test can-fuse in alcuni casi in cui non è ansible eseguire cmp , ad es. con js .

Quasi tutte le semplici operazioni ALU (bitwise booleano, add / sub, ecc.) Vengono eseguite in un singolo ciclo. Hanno tutti lo stesso “costo” nel rintracciarli attraverso la pipeline di esecuzione fuori ordine. Intel e AMD spendono i transistor per rendere le unità di esecuzione veloci per aggiungere / sub / qualsiasi cosa in un singolo ciclo. Sì, OR bit a bit o AND è più semplice e probabilmente utilizza meno energia, ma non può essere eseguito più velocemente di un ciclo di clock.


Inoltre, come sottolinea Brendan, or reg, reg aggiunge un altro ciclo di latenza alla catena delle dipendenze per le seguenti istruzioni che devono leggere il registro.

Tuttavia, su CPU P6-family (da PPro / PII a Nehalem), scrivere il registro di destinazione può effettivamente essere un vantaggio . Esiste un numero limitato di porte di lettura del registro per la fase di rilascio / ridenominazione da leggere dal file di registro permanente, ma i valori scritti di recente sono disponibili direttamente dal ROB. Riscrivere un registro inutilmente può renderlo di nuovo disponibile nella rete di inoltro per evitare blocchi di lettura della registrazione. (Vedi il pdf di microar di Agner Fog .

Il compilatore di Delphi, a quanto riferito, utilizza or eax,eax , che era una scelta ragionevole al momento, partendo dal presupposto che le bancarelle di lettura dei registri fossero più importanti dell’allungamento della catena di dep per qualsiasi cosa lesse in seguito.

Sfortunatamente, i compilatori-scrittori al momento non conoscevano il futuro, perché and eax,eax comporta esattamente in modo equivalente a or eax,eax su Intel P6-family, ma è meno male su altri uarches perché and può fungere da macro su Sandybridge- famiglia.

Per Core2 / Nehalem (gli ultimi 2 uarches della famiglia P6), il test può fondersi con le macro ma non può, quindi (a differenza del Pentium II / III / M) è un compromesso tra la macro-fusione e possibilmente la riduzione del registro leggi bancarelle. L’evitamento del registro di lettura-stallo comporta ancora il costo della latenza aggiuntiva se il valore viene letto dopo essere stato testato, quindi il test può essere una scelta migliore rispetto and in alcuni casi anche prima di un cmov o setcc , non un jcc , o su CPU senza macro-fusione.

Se stai sintonizzando qualcosa per essere veloce su più interfacce, usa il test meno che la profilatura non dimostri che le bancarelle di lettura dei registri rappresentano un grosso problema in un caso specifico su Core2 / Nehalem, e l’utilizzo and correzione effettiva.

IDK da cui proviene il linguaggio or reg,reg tranne forse che è più breve da digitare. O forse è stato usato di proposito per le CPU P6 per riscrivere deliberatamente un registro prima di usarlo ancora. I codificatori al momento non potevano prevedere che sarebbe stato meno efficiente di and per quello scopo. Ma ovviamente non dovremmo mai usarlo su test o in un nuovo codice. (C’è solo una differenza quando è immediatamente prima di un jcc su Sandybridge-family, ma è più semplice da dimenticare or reg,reg .)


Per testare un valore in memoria , va bene a cmp dword [mem], 0 , ma le CPU Intel non possono eseguire il fusion macro delle istruzioni di flag che hanno sia un operando immediato che un operando di memoria. Se hai intenzione di usare il valore dopo il confronto in un lato del ramo, dovresti probabilmente mov eax, [mem] / test eax,eax o qualcosa del genere. In caso contrario (es. Test di un booleano), cmp con un operando di memoria va bene.

Sebbene si noti che alcune modalità di indirizzamento non si microfonteranno sulla famiglia SnB : RIP-relativo + immediato non si microfoni nei decodificatori, o le modalità di indirizzamento indicizzate si annullano. In entrambi i casi, si ottengono 3 uops di dominio fuso per cmp dword [rsi + rcx*4], 0 / jne o [rel some_static_location] .

È anche ansible testare un valore in memoria con test dword [mem], -1 , ma non farlo. Poiché test r/m16/32/64, sign-extended-imm8 non è disponibile, è peggio della dimensione del codice di cmp per qualcosa di più grande di byte. (Penso che l’idea progettuale fosse che se si vuole solo testare il bit basso di un registro, basta test cl, 1 invece di test ecx, 1 , e usare casi come test ecx, 0xfffffff0 sono abbastanza rari da non essere vale la pena spendere un codice operativo, soprattutto perché la decisione è stata presa per 8086 con codice a 16 bit, dove era solo la differenza tra un imm8 e imm16, non imm32).

Ho scritto -1 piuttosto che 0xFFFFFFFF quindi sarebbe lo stesso con byte o qword . ~0 sarebbe un altro modo per scriverlo.