2011-10-08 15 views
12

qualcuno mi ha mostrato alcuni anni fa il seguente comando per azzerare una variabile.zero assegnazione contro xor, il secondo è davvero più veloce?

xor i,i 

Mi ha detto che questo è più veloce dell'assegnazione di zero ad esso. È vero? I compilatori fanno ottimizzazione per ottenere il codice per eseguire una cosa del genere?

+0

possibile duplicato di [Usando xor reg, reg si avvantaggia il reg di mov, 0?] (Http://stackoverflow.com/questions/1135679/does-using-xor-reg-reg-give-advantage-over -mov-reg-0) –

+0

['xor eax, eax' è il modo migliore per azzerare un registro in x86 asm (per molte ragioni, non solo per la dimensione del codice)] (http://stackoverflow.com/questions/33666617/what-is-the-best-way-to-set-a-register-to-zero-in-x86-assembly-xor-mov-or-and), ma nel codice sorgente C dovresti sempre scrivere 'var = 0; 'e lascia che il compilatore usi xor per te. Non scrivere 'var^= var', perché ha zero vantaggi e molti possibili svantaggi (ad esempio, sconfiggere l'ottimizzatore, specialmente se var non è inizializzato). Solo postare un commento perché questa domanda sembra essere confusa circa la richiesta di asm rispetto all'input del compilatore. –

risposta

25

Si può provare questo voi stessi per vedere la risposta:

movl $0,%eax 
    xor %eax,%eax 

assemblare poi smontare:

as xor.s -o xor.o 
objdump -D xor.o 

E ottenere

0: b8 00 00 00 00   mov $0x0,%eax 
    5: 31 c0     xor %eax,%eax 

l'istruzione MOV per un registro a 32 bit è 2,5 volte più grande, richiede più tempo per caricare da RAM e consuma molto più spazio nella cache. Nel giorno in cui il tempo di caricamento da solo era un killer, oggi il tempo del ciclo di memoria e lo spazio della cache potrebbero non essere così evidenti, ma è se il compilatore e/o il codice lo fanno troppo spesso vedrete la perdita di cache spazio e/o più sfratti e più, lento, cicli di memoria di sistema.

Nelle moderne CPU, dimensioni del codice più grandi possono anche rallentare i decodificatori, forse impedendo loro di decodificare il loro numero massimo di istruzioni x86 per ciclo. (ad esempio fino a 4 istruzioni in un blocco 16B per alcune CPU.)

Ci sono anche performance advantages to xor over mov in some x86 CPUs (especially Intel's) that have nothing to do with code-size, quindi xor-zeroing è sempre preferito nell'assemblaggio x86.


Un'altra serie di esperimenti:

void fun1 (unsigned int *a) 
{ 
    *a=0; 
} 
unsigned int fun2 (unsigned int *a, unsigned int *b) 
{ 
    return(*a^*b); 
} 
unsigned int fun3 (unsigned int a, unsigned int b) 
{ 
    return(a^b); 
} 


0000000000000000 <fun1>: 
    0: c7 07 00 00 00 00  movl $0x0,(%rdi) 
    6: c3      retq 
    7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) 
    e: 00 00 

0000000000000010 <fun2>: 
    10: 8b 06     mov (%rsi),%eax 
    12: 33 07     xor (%rdi),%eax 
    14: c3      retq 
    15: 66 66 2e 0f 1f 84 00 nopw %cs:0x0(%rax,%rax,1) 
    1c: 00 00 00 00 

0000000000000020 <fun3>: 
    20: 89 f0     mov %esi,%eax 
    22: 31 f8     xor %edi,%eax 
    24: c3      retq 

capi giù per il sentiero di mostrare ciò che a variabili XOR io, io come nella sua domanda potrebbe portare a.Dato che non hai specificato quale processore o quale contesto ti stavi riferendo, è difficile dipingere l'intera immagine. Se per esempio stai parlando di codice C, devi capire cosa fanno i compilatori per quel codice, e questo dipende pesantemente dal codice nella funzione stessa, se al momento della tua xor il compilatore ha l'operando in un registro e dipende sulle impostazioni del compilatore potresti ottenere xor eax, eax. oppure il compilatore può scegliere di cambiarlo in un reg di movimento, 0, o cambiare qualcosa = 0; a un reg xor, reg.

Alcune altre sequenze per riflettere:

se l'indirizzo alla variabile è già in un registro:

7: c7 07 00 00 00 00  movl $0x0,(%rdi) 

    d: 8b 07     mov (%rdi),%eax 
    f: 31 c0     xor %eax,%eax 
    11: 89 07     mov %eax,(%rdi) 

Il compilatore sceglierà il mov zero invece del xor. Che è quello che si otterrebbe se si è tentato di questo codice C:

void funx (unsigned int *a) 
{ 
    *a=*a^*a; 
} 

Il compilatore sostituisce con una mossa a zero. È stato recuperato lo stesso numero di byte, ma è stata necessaria una seconda memoria invece di una, e un registro masterizzato. e tre istruzioni da eseguire invece di una. Quindi lo spostamento zero è sensibilmente migliore.

Ora, se si tratta di byte di dimensioni e in un registro:

13: b0 00     mov $0x0,%al 
15: 30 c0     xor %al,%al 

alcuna differenza nella dimensione del codice. (Ma eseguono ancora in modo diverso).


Ora se si stesse parlando di un altro processore, consente di dire ARM

0: e3a00000 mov r0, #0 
    4: e0200000 eor r0, r0, r0 
    8: e3a00000 mov r0, #0 
    c: e5810000 str r0, [r1] 
    10: e5910000 ldr r0, [r1] 
    14: e0200000 eor r0, r0, r0 
    18: e5810000 str r0, [r1] 

Tu non salvare nulla utilizzando lo XOR (EOR esclusivo o,): un'istruzione è un'istruzione sia inverosimile e l'esecuzione . xoring qualcosa in ram, proprio come qualsiasi processore se si ha l'indirizzo della variabile in un registro. Se devi copiare i dati in un altro registro per eseguire l'xor, allora ti ritroverai con due accessi alla memoria e tre istruzioni. Se hai un processore che può fare memoria in memoria, lo spostamento di zero è più economico perché hai solo l'accesso alla memoria e una o due istruzioni a seconda del processore.

In realtà è peggio di quello: eor r0, r0, r0 è required to have an input dependency on r0 (limitando l'esecuzione fuori servizio), a causa delle regole di ordinamento della memoria. Xor-zeroing produce sempre zero, ma aiuta solo le prestazioni nell'assemblaggio x86.


Così la linea di fondo è che dipende, se si sta parlando registri in assembler su un sistema x86 ovunque dal 8088 ad oggi la XOR è spesso più veloce, perché l'istruzione è più piccolo, recupera più veloce, richiede meno cache se ne hai uno, lascia più cache per altro codice, ecc. Allo stesso modo processori di lunghezza delle istruzioni variabili non x86 che richiedono lo zero per essere codificati nell'istruzione richiederanno anche un'istruzione più lunga, tempo di recupero più lungo, più cache consumata se c'è una cache , ecc. Quindi il xor è più veloce (di solito, dipende da come codifica). Diventa molto peggio se disponi di flag condizionali e vuoi che move/xor imposti il ​​flag zero, potresti dover masterizzare l'istruzione corretta (su alcuni processori il mov non cambia i flag). Alcuni processori hanno un registro di zero speciale, che non è di uso generale, quando lo si utilizza si ottiene uno zero in questo modo è possibile codificare questo caso d'uso molto comune senza bruciare più spazio di istruzioni o masterizzare un ciclo di istruzioni aggiuntivo caricando uno zero immediato in un registro . msp430 per esempio, uno spostamento di 0x1234 ti costerebbe un'istruzione a due parole, ma spostare 0x0000 o 0x0001 e poche altre costanti possono essere codificate in una singola parola di istruzioni.Tutti i processori avranno il doppio hit in memoria se si sta parlando di una variabile in ram, leggere-modify-write due cicli di memoria senza contare i recuperi dell'istruzione e peggiora se la lettura causa un riempimento della linea cache (la scrittura sarebbe quindi molto veloce), ma senza la lettura la sola scrittura potrebbe passare direttamente dalla cache ed eseguire molto velocemente mentre il processore potrebbe continuare a funzionare mentre la scrittura procedeva in parallelo (a volte si ottiene un miglioramento delle prestazioni, a volte no, sempre se si sintonizza per questo). I processori x86 e probabilmente precedenti sono la ragione per cui si vede l'abitudine di xoring invece di spostare zero. Il guadagno di prestazioni è ancora lì oggi per quelle ottimizzazioni specifiche, la memoria di sistema è ancora estremamente lenta e ogni ciclo di memoria extra è costoso, allo stesso modo qualsiasi cache che viene buttata via è costosa. I compilatori decenti a metà, anche gcc, rileveranno un xor i, che è equivalente a i = 0 e scelgono, caso per caso, la sequenza di istruzioni migliore (su un sistema medio).

Prendi una copia dello Zen of Assembly di Michael Abrash. Le copie buone e usate sono disponibili a un prezzo ragionevole (meno di $ 50), anche se si ottengono le copie da $ 80 ne vale la pena. Cerca di guardare oltre i particolari 8088 "mangiatori di cicli" e capire il processo generale di pensiero che sta cercando di insegnare. Trascorri quindi tutto il tempo che puoi per smontare il tuo codice, idealmente per molti processori diversi. Applica ciò che hai appreso ...

+0

ottima risposta! – stdcall

5

Su CPU più vecchie (ma quelle successive al Pentium Pro, come da commenti) questo era il caso, tuttavia, la maggior parte delle CPU moderne in questi giorni ha percorsi hot speciali per l'assegnazione zero (di registri e variabili ben allineate) che dovrebbe fornire prestazioni equivalenti. i compilatori più moderni tenderanno a utilizzare un mix dei due, a seconda del codice circostante (i compilatori MSVC più vecchi utilizzerebbero sempre XOR in build ottimizzati e utilizza ancora XOR un po ', ma utilizzerà anche MOV reg,0 in determinate circostanze).

Questa è una micro ottimizzazione, quindi, si può semplicemente fare ciò che è meglio per voi, a meno che non vi siano cicli stretti che sono in ritardo a causa delle dipendenze dei registri. tuttavia, si noti che l'uso di XOR occupa meno spazio la maggior parte del tempo, il che è ottimo per i dispositivi incorporati o quando si tenta di allineare un target di diramazione.

questo presuppone che ci si riferisca principalmente a x86 e alle sue derivate, su quella nota @Pascal mi ha dato l'idea di inserire i riferimenti tecnici che alla base di ciò. Il manuale di ottimizzazione Intel ha due sezioni che trattano di questo, vale a dire, 2.1.3.1 Dependancy Breaking Idioms e 3.5.1.7 Clearing Registers and Dependancy Breaking Idioms. Queste due sezioni sostengono basicamente l'uso delle istruzioni basate su XOR per qualsiasi forma di cancellazione del registro a causa della sua natura di dipendenza (che rimuove la latenza). Tuttavia, nelle sezioni in cui i codici condizionali devono essere conservati, è preferibile inserire MOV 0 in un registro.

+2

Non ho ** no ** idea di cosa intendi per "percorsi a caldo per assegnazione zero". Potete fornire un riferimento? Come nota a margine, 'xor reg, reg' era più lento di' mov reg, 0' sul Pentium Pro, perché il processore pensava che il primo avesse una dipendenza da 'reg'. Prima di questo, non esisteva alcuna esecuzione Out of Order in questa famiglia di processori e, successivamente, i processori hanno imparato a riconoscere 'xor reg, reg' come indipendente rispetto al valore precedente di' reg'. –

+1

@Pascal: Con "percorsi a caldo per l'assegnazione zero" intendevo che il micro-codice è ottimizzato per fare ciò con una latenza minima (rompendo le dipendenze come hai menzionato) – Necrolis

+5

Su SandyBridge, xor-zeroing è di tipo speciale e gestito dal registro rinomina, non usa nemmeno una porta di esecuzione. Non ho mai sentito nulla su trucchi simili che si applicano a 'mov reg, 0', ma sarebbe bello se esistessero, hai una fonte per questo? – harold

0

Definitivamente era vero per l'8088 (e in misura minore per l'8086) a causa dell'istruzione xor che si accorcia e della coda di prefetch alle limitazioni della larghezza di banda della memoria.

Problemi correlati