Usando LEA su valori che non sono indirizzi / puntatori?

Stavo cercando di capire come funziona l’Istruzione di calcolo degli indirizzi, specialmente con il comando leaq. Quindi mi confondo quando vedo esempi che usano leaq per fare calcoli aritmetici. Ad esempio, il seguente codice c,

long m12(long x) { return x*12; } 

In assemblea,

 leaq (%rdi, %rdi, 2), %rax salq $2, $rax 

Se ho capito bene, leaq dovrebbe spostare qualunque indirizzo (% rdi,% rdi, 2), che dovrebbe essere 2 *% rdi +% rdi, valutare in% rax. Quello che mi confonde è dato dal fatto che il valore x è memorizzato in% rdi, che è solo l’indirizzo di memoria, perché i tempi% rdi per 3 poi a sinistra spostano questo indirizzo di memoria di 2 è uguale a x volte 12? Non è così quando torniamo a% rdi per 3, saltiamo su un altro indirizzo di memoria che non ha valore x?

leaq non deve operare sugli indirizzi di memoria, e calcola un indirizzo, in realtà non legge dal risultato, quindi finché un mov o simili prova a usarlo, è solo un modo esoterico per aggiungere un numero, più 1, 2, 4 o 8 volte un altro numero (o lo stesso numero in questo caso). È abusato frequentemente per scopi matematici, come vedi. 2*%rdi+%rdi è solo 3 * %rdi , quindi calcola x * 3 senza coinvolgere l’unità moltiplicatore sulla CPU.

Allo stesso modo, lo spostamento a sinistra, per interi, raddoppia il valore per ogni bit spostato (ogni zero aggiunto a destra), grazie al modo in cui funzionano i numeri binari (allo stesso modo nei numeri decimali, aggiungendo gli zeri a destra moltiplica per 10).

Quindi questo sta abusando dell’istruzione leaq per ottenere la moltiplicazione per 3, quindi spostando il risultato per ottenere un’ulteriore moltiplicazione per 4, per un risultato finale di moltiplicare per 12 senza mai effettivamente usare un’istruzione moltiplicata (che presumibilmente crede sarebbe più lenta, e per quanto ne so, potrebbe essere giusto, la seconda ipotesi è che il compilatore di solito è un gioco perdente).

lea (vedere la voce del manuale di istruzioni di Intel) è un’istruzione shift-and-add che utilizza la syntax dell’operando della memoria e la codifica della macchina. Questo spiega il nome, ma non è l’unica cosa per cui va bene. In realtà non accede mai alla memoria, quindi è come usare & in C.

Vedi ad esempio Come moltiplicare un registro per 37 usando solo 2 istruzioni leal consecutive in x86?

In C, è come uintptr_t foo = &arr[idx] . Si noti & per darvi il risultato di arr + idx , incluso il ridimensionamento per la dimensione dell’object di arr . In C, questo sarebbe un abuso della syntax e dei tipi di linguaggio, ma in x86 i puntatori e gli interi sono la stessa cosa. Tutto è solo byte, e spetta al programma mettere le istruzioni nell’ordine giusto per ottenere risultati utili.


Il progettista / architetto originale del set di istruzioni 8086 ( Stephen Morse ) poteva o non poteva avere in mente la matematica dei puntatori come caso d’uso principale, ma i compilatori moderni la considerano semplicemente un’altra opzione per fare aritmetica sui puntatori / interi, e questo è come dovresti pensarci anche tu.

(Si noti che le modalità di indirizzamento a 16 bit non includono i turni, solo [BP|BX] + [SI|DI] + disp8/disp16 , quindi LEA non era utile per la matematica senza puntatore prima del 386. Vedere questa risposta per maggiori informazioni sulle modalità di indirizzamento a [rax + rdi*4] bit, sebbene tale risposta utilizzi la syntax Intel come [rax + rdi*4] invece della syntax AT & T utilizzata in questa domanda. Il codice macchina x86 è lo stesso indipendentemente dalla syntax utilizzata per crearlo. )

Forse gli architetti 8086 hanno semplicemente voluto esporre l’hardware di calcolo degli indirizzi per usi arbitrari perché potevano farlo senza utilizzare molti transistor aggiuntivi. Il decoder deve già essere in grado di decodificare le modalità di indirizzamento e altre parti della CPU devono essere in grado di eseguire calcoli di indirizzi. Mettere il risultato in un registro invece di usarlo con un valore di registro di segmento per l’accesso alla memoria non richiede molti transistor aggiuntivi. Ross Ridge conferma che LEA nell’8086 originale riutilizza le CPU con decodifica e hardware di calcolo efficaci.


Si noti che la maggior parte delle CPU moderne eseguono LEA sulle stesse ALU delle normali istruzioni di aggiunta e triggerszione . Hanno AGU dedicate (unità di generazione di indirizzi), ma le usano solo per gli operandi di memoria reali. Atom in-order è un’eccezione; LEA viene eseguito in precedenza nella pipeline rispetto alle ALU: gli input devono essere pronti prima, ma anche gli output sono pronti prima. Le CPU di esecuzione fuori ordine (la stragrande maggioranza dei moderni x86) non vogliono che LEA interferisca con carichi / negozi effettivi, quindi lo eseguono su una ALU.

lea ha una buona latenza e velocità, ma non un buon throughput come add o mov r32, imm32 sulla maggior parte delle CPU, quindi usa solo lea quando puoi salvare le istruzioni con esso invece di add . (Vedi la guida microarco di x86 di Agner e il manuale di ottimizzazione asm ).


L’implementazione interna è irrilevante, ma è una scommessa sicura che la decodifica degli operandi in LEA condivide i transistor con modalità di indirizzamento di decodifica per qualsiasi altra istruzione . (Quindi c’è riutilizzo / condivisione dell’hardware anche su CPU moderne che non eseguono lea su un AGU.) Qualsiasi altro modo di esporre un’istruzione di shift-and-add multi-input avrebbe richiesto una codifica speciale per gli operandi.

Quindi 386 ottenne un’istruzione ALU shift-and-add per “free” quando estese le modalità di indirizzamento per includere l’indice in scala, e la possibilità di usare qualsiasi registro in una modalità di indirizzamento rese LEA molto più facile da usare anche per i non puntatori .

x86-64 ha ottenuto un accesso economico al contatore del programma ( invece di dover leggere la call spinta ) “gratuitamente” tramite LEA perché ha aggiunto la modalità di indirizzamento relativa al RIP, rendendo l’accesso ai dati statici decisamente più economico in x86-64 indipendente dalla posizione codice rispetto a PIC a 32 bit. (Il parente del RIP ha bisogno di un supporto speciale nelle ALU che gestiscono LEA, così come le AGU separate che gestiscono gli indirizzi di carico / negozio effettivi. Ma nessuna nuova istruzione era necessaria).


È altrettanto valido per l’aritmetica arbitraria come per i puntatori, quindi è un errore pensarlo come previsto per i puntatori in questi giorni . Non è un “abuso” o un “trucco” usarlo per i non puntatori, perché tutto è un numero intero nel linguaggio assembly. Ha un throughput inferiore rispetto add , ma è abbastanza economico da usarlo quasi sempre quando salva anche solo un’istruzione. Ma può salvare fino a tre istruzioni:

 ;; Intel syntax. lea eax, [rdi + rsi*4 - 8] ; 3 cycle latency on Intel SnB-family ; 2-component LEA is only 1c latency ;;; without LEA: mov eax, esi ; maybe 0 cycle latency, otherwise 1 shl eax, 2 ; 1 cycle latency add eax, edi ; 1 cycle latency sub eax, 8 ; 1 cycle latency 

Su alcune CPU AMD, anche un LEA complesso ha una latenza di solo 2 cicli, ma la sequenza di 4 istruzioni sarebbe una latenza di 4 cicli da quando esi è pronto per essere pronto per l’ eax finale. Ad ogni modo, questo salva 3 UOP per il front-end da decodificare e rilasciare, e che occupano spazio nel buffer di riordino fino al pensionamento.

lea ha diversi importanti vantaggi , in particolare nel codice 32/64-bit in cui le modalità di indirizzamento possono utilizzare qualsiasi registro e possono spostarsi:

  • non distruttivo: uscita in un registro che non è uno degli input . A volte è utile come copia e aggiungi come lea 1(%rdi), %eax o lea (%rdx, %rbp), %ecx .
  • può fare 3 o 4 operazioni in una sola istruzione (vedi sopra).
  • La matematica senza modificare EFLAGS può essere utile dopo un test prima di un cmovcc . O magari in un ciclo add-with-carry sulle CPU con stallo a bandiera parziale.
  • x86-64: il codice indipendente dalla posizione può utilizzare un LEA relativo al RIP per ottenere un puntatore ai dati statici.

    7-byte lea foo(%rip), %rdi è leggermente più grande e più lento di mov $foo, %edi (5 byte), quindi preferisci mov r32, imm32 in codice dipendente dalla posizione su sistemi operativi in ​​cui i simboli sono nei 32 bit bassi di spazio degli indirizzi virtuali, come Linux. Potrebbe essere necessario disabilitare l’impostazione PIE predefinita in gcc per usarlo.

    Nel codice a 32 bit, mov edi, OFFSET symbol è similmente più corto e più veloce di lea edi, [symbol] . (Lasciare fuori OFFSET nella syntax NASM.) RIP-relativo non è disponibile e gli indirizzi rientrano in un immediato a 32 bit, quindi non c’è motivo di considerare lea invece di mov r32, imm32 se è necessario ottenere indirizzi di simboli statici in registri .

A parte il LEA relativo al RIP in modalità x86-64, tutti questi si applicano allo stesso modo al calcolo dei puntatori rispetto al calcolo dell’aggiunta / spostamento dei numeri interi senza puntatore.

Vedi anche il wiki del tag x86 per guide / manuali di assemblaggio e informazioni sulle prestazioni.


Operand-size vs. address-size per x86-64 lea

Vedi anche Le operazioni integer del complemento 2 che possono essere utilizzate senza azzerare i bit alti negli ingressi, se si desidera solo la parte bassa del risultato? . La dimensione degli indirizzi a 64 bit e le dimensioni degli operandi a 32 bit è la codifica più compatta (nessun prefisso aggiuntivo), quindi preferisci lea (%rdx, %rbp), %ecx quando ansible invece di 64-bit lea (%rdx, %rbp), %rcx o 32- lea (%edx, %ebp), %ecx .

x86-64 lea (%edx, %ebp), %ecx è sempre uno spreco di un prefisso dimensione-indirizzo vs lea (%rdx, %rbp), %ecx , ma la dimensione di operando / indirizzo 64-bit è ovviamente richiesta per facendo matematica a 64 bit. (Il disassemblatore objconv di Agner Fog mette addirittura in guardia sui prefissi di dimensioni dell’indirizzo inutili su LEA con una dimensione dell’operando di 32 bit.)

Tranne forse su Ryzen, dove Agner Fog segnala che la dimensione di operando a 32 bit nella modalità a 64 bit ha un ulteriore ciclo di latenza. Non so se sovrascrivere la dimensione dell’indirizzo a 32 bit possa accelerare LEA in modalità 64 bit se ne hai bisogno per troncare a 32 bit.


Questa domanda è quasi un duplicato di ciò che è altamente votato Qual è lo scopo dell’istruzione LEA? , ma la maggior parte delle risposte lo spiegano in termini di calcolo dell’indirizzo sui dati dei puntatori effettivi. Questo è solo un uso.

LEA serve per calcolare l’indirizzo . Non dereferenzia l’indirizzo di memoria

Dovrebbe essere molto più leggibile nella syntax Intel

 m12(long): lea rax, [rdi+rdi*2] sal rax, 2 ret 

Quindi la prima riga equivale a rax = rdi*3 Quindi lo spostamento a sinistra è moltiplicare rax per 4, il che si traduce in rdi*3*4 = rdi*12