Perché le istruzioni x86-64 sui registri a 32 bit azzerano la parte superiore del registro a 64 bit completo?


119

Nel tour x86-64 dei manuali Intel , ho letto

Forse il fatto più sorprendente è che un'istruzione come MOV EAX, EBXazzera automaticamente i 32 bit di RAXregistro superiori.

La documentazione Intel (3.4.1.1 General-Purpose Registers in 64-Bit Mode in manual Basic Architecture) citata nella stessa fonte ci dice:

  • Gli operandi a 64 bit generano un risultato a 64 bit nel registro generico di destinazione.
  • Gli operandi a 32 bit generano un risultato a 32 bit, esteso da zero a un risultato a 64 bit nel registro generico di destinazione.
  • Gli operandi a 8 e 16 bit generano un risultato a 8 o 16 bit. I 56 bit superiori o 48 bit (rispettivamente) del registro di destinazione generale non vengono modificati dall'operazione. Se il risultato di un'operazione a 8 o 16 bit è destinato al calcolo dell'indirizzo a 64 bit, estendi con segno esplicito il registro a 64 bit completi.

Nell'assembly x86-32 e x86-64, istruzioni a 16 bit come

mov ax, bx

non mostrare questo tipo di comportamento "strano" che la parola superiore di eax sia azzerata.

Quindi: qual è il motivo per cui è stato introdotto questo comportamento? A prima vista sembra illogico (ma il motivo potrebbe essere che sono abituato alle stranezze dell'assembly x86-32).


16
Se cerchi su Google "Blocco registro parziale", troverai molte informazioni sul problema che stavano (quasi certamente) cercando di evitare.
Jerry Coffin


4
Non solo "la maggior parte". Per quanto ne so, tutte le istruzioni con un r32operando di destinazione zero il 32, piuttosto che unire. Ad esempio, alcuni assemblatori sostituiranno pmovmskb r64, xmmcon pmovmskb r32, xmm, salvando un REX, perché la versione di destinazione a 64 bit si comporta in modo identico. Anche se la sezione Operazione del manuale elenca separatamente tutte le 6 combinazioni di 32 / 64bit dest e 64/128 / 256b sorgente, l'estensione zero implicita del modulo r32 duplica l'estensione zero esplicita del modulo r64. Sono curioso dell'implementazione HW ...
Peter Cordes

2
@HansPassant, inizia il riferimento circolare.
kchoi

Risposte:


98

Non sono AMD né parlo per loro, ma l'avrei fatto allo stesso modo. Perché l'azzeramento della metà alta non crea una dipendenza dal valore precedente, su cui la CPU dovrebbe attendere. Il meccanismo di ridenominazione dei registri verrebbe sostanzialmente annullato se non fosse stato fatto in questo modo.

In questo modo è possibile scrivere codice veloce utilizzando valori a 32 bit in modalità 64 bit senza dover interrompere esplicitamente le dipendenze per tutto il tempo. Senza questo comportamento, ogni singola istruzione a 32 bit in modalità 64 bit dovrebbe attendere qualcosa che è accaduto prima, anche se quella parte alta non verrebbe quasi mai utilizzata. (Fare int64 bit sprecherebbe il footprint della cache e la larghezza di banda della memoria; x86-64 supporta in modo più efficiente le dimensioni degli operandi a 32 e 64 bit )

Il comportamento per le dimensioni degli operandi a 8 e 16 bit è strano. La follia della dipendenza è uno dei motivi per cui le istruzioni a 16 bit vengono ora evitate. x86-64 lo ha ereditato da 8086 per 8 bit e 386 per 16 bit e ha deciso che i registri a 8 e 16 bit funzionassero allo stesso modo in modalità 64 bit come in modalità 32 bit.


Vedi anche Perché GCC non usa registri parziali? per dettagli pratici su come le scritture su registri parziali a 8 e 16 bit (e le successive letture del registro completo) siano gestite da CPU reali.


8
Non penso sia strano, penso che non volessero rompere troppo e hanno mantenuto il vecchio comportamento lì.
Alexey Frunze

5
@Alex quando hanno introdotto la modalità a 32 bit, non c'era un vecchio comportamento per la parte alta. Non c'era una parte alta prima .. Ovviamente dopo non poteva più essere cambiata.
Harold

1
Stavo parlando di operandi a 16 bit, perché i primi bit non vengono azzerati in quel caso. Non in modalità non a 64 bit. E anche in modalità a 64 bit.
Alexey Frunze

3
Ho interpretato il tuo "Il comportamento per le istruzioni a 16 bit è strano" come "è strano che l'estensione zero non avvenga con operandi a 16 bit in modalità 64 bit". Da qui i miei commenti su come mantenerlo allo stesso modo in modalità a 64 bit per una migliore compatibilità.
Alexey Frunze

8
@Alex oh capisco. Ok. Non credo sia strano da quella prospettiva. Solo da una prospettiva "guardando indietro, forse non è stata una buona idea". Immagino che avrei dovuto essere più chiaro :)
Harold

9

Risparmia semplicemente spazio nelle istruzioni e nel set di istruzioni. È possibile spostare piccoli valori immediati in un registro a 64 bit utilizzando le istruzioni esistenti (a 32 bit).

Inoltre ti evita di dover codificare valori di 8 byte per MOV RAX, 42, quando MOV EAX, 42possono essere riutilizzati.

Questa ottimizzazione non è così importante per le operazioni a 8 e 16 bit (perché sono più piccole) e la modifica delle regole potrebbe anche rompere il vecchio codice.


7
Se è corretto, non avrebbe avuto più senso estendere il segno piuttosto che estendere 0?
Damien_The_Unbeliever

16
L'estensione del segno è più lenta, anche nell'hardware. L'estensione zero può essere eseguita in parallelo con qualsiasi calcolo produca la metà inferiore, ma l'estensione del segno non può essere eseguita finché (almeno il segno di) la metà inferiore non è stata calcolata.
Jerry Coffin

13
Un altro trucco correlato è da usare XOR EAX, EAXperché XOR RAX, RAXavrebbe bisogno di un prefisso REX.
Neil

3
@Nubok: certo, avrebbero potuto aggiungere una codifica di movzx / movsx che richiede un argomento immediato. Il più delle volte è più conveniente avere i bit superiori azzerati, quindi puoi usare un valore come indice di array (perché tutti i reg devono avere la stessa dimensione in un indirizzo effettivo: [rsi + edx]non è consentito). Ovviamente evitare false dipendenze / stalli di registri parziali (l'altra risposta) è un altro motivo importante.
Peter Cordes

4
e cambiando le regole si infrangerebbe anche il vecchio codice. Il vecchio codice non può comunque essere eseguito in modalità a 64 bit (ad esempio, inc / dec a 1 byte sono prefissi REX); questo è irrilevante. Il motivo per non ripulire le verruche di x86 è il minor numero di differenze tra la modalità lunga e le modalità compat / legacy, quindi meno istruzioni devono essere decodificate in modo diverso a seconda della modalità. AMD non sapeva che AMD64 avrebbe preso piede e sfortunatamente era molto conservatrice, quindi ci sarebbero voluti meno transistor per supportare. A lungo termine, sarebbe andato bene se i compilatori e gli umani dovessero ricordare quali cose funzionano in modo diverso nella modalità a 64 bit.
Peter Cordes

1

Senza che lo zero si estenda a 64 bit, significherebbe che un'istruzione che legge da raxavrebbe 2 dipendenze per il suo raxoperando (l'istruzione che scrive eaxe l'istruzione che scrive raxprima di essa), ciò significa che 1) il ROB dovrebbe avere voci per più dipendenze per un singolo operando, il che significa che ROB richiederebbe più logica e transistor e occuperebbe più spazio, e l'esecuzione sarebbe più lenta in attesa di una seconda dipendenza non necessaria che potrebbe richiedere anni per essere eseguita; o in alternativa 2), cosa che immagino accada con le istruzioni a 16 bit, la fase di allocazione probabilmente si blocca (cioè se il RAT ha un'allocazione attiva per una axscrittura e eaxappare una lettura, si blocca fino a quando la axscrittura non si ritira).

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

L'unico vantaggio di non estendere zero è garantire che i bit di ordine superiore raxsiano inclusi, ad esempio, se originariamente contiene 0xffffffffffffffff, il risultato sarebbe 0xffffffff00000007, ma c'è pochissimo motivo per l'ISA di rendere questa garanzia a tale costo, e è più probabile che il vantaggio dell'estensione zero sia effettivamente richiesto di più, quindi salva la riga di codice aggiuntiva mov rax, 0. Garantendo che sarà sempre zero esteso a 64 bit, i compilatori possono lavorare con questo assioma in mente mentre sono dentro mov rdx, rax, raxdevono solo aspettare la sua singola dipendenza, il che significa che può iniziare l'esecuzione più velocemente e ritirarsi, liberando unità di esecuzione. Inoltre, consente anche idiomi zero più efficienti come xor eax, eaxzero raxsenza richiedere un byte REX.


Almeno i flag parziali su Skylake funzionano avendo input separati per CF rispetto a qualsiasi SPAZO. (Quindi cmovbeè 2 uops ma cmovbè 1). Ma nessuna CPU che rinomina i registri parziali lo fa nel modo suggerito. Invece inseriscono un uop di fusione se un registro parziale viene rinominato separatamente dal registro completo (cioè è "sporco"). Vedi Perché GCC non usa i registri parziali? e come si comportano esattamente le registrazioni parziali su Haswell / Skylake? Scrivere AL sembra avere una falsa dipendenza da RAX e AH è incoerente
Peter Cordes

Le CPU della famiglia P6 si sono bloccate per ~ 3 cicli per inserire un uop (Core2 / Nehalem), o le precedenti famiglie P6 (PM, PIII, PII, PPro) si sono fermate per (almeno?) ~ 6 cicli. Forse è come suggerito in 2, in attesa che il valore reg completo sia disponibile tramite writeback nel file di registro permanente / architettonico.
Peter Cordes

@ PeterCordes oh, sapevo di unire gli uops almeno per gli stalli di bandiere parziali. Ha senso, ma ho dimenticato come funziona per un minuto; è scattato una volta ma ho dimenticato di prendere appunti
Lewis Kelsey

@PeterCordes microarchitecture.pdf: This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAXNon riesco a trovare un esempio del "merging uop" che verrebbe utilizzato per risolvere questo problema, lo stesso per uno stallo parziale della bandiera
Lewis Kelsey

Bene, i primi P6 si bloccano fino al writeback. Core2 e Nehalem inseriscono una fusione dopo / prima? bloccando solo il front-end per un tempo più breve. Sandybridge inserisce unendo gli uops senza fermarsi. (Ma l'unione AH deve emettere in un ciclo da sola, mentre la fusione AL può essere parte di un gruppo completo.) Haswell / SKL non rinomina affatto AL separatamente da RAX, quindi mov al, [mem]è un carico microfuso + ALU- merge, rinominando solo AH, e un uop di AH-merging continua ancora da solo. I meccanismi di unione dei flag parziali in queste CPU variano, ad esempio Core2 / Nehalem si blocca ancora per i flag parziali, a differenza del registro parziale.
Peter Cordes
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.