Con i costi implementativi trattati dal precedente articolo si concludono le osservazioni e le critiche, mentre adesso vengono esposte possibili migliorie che potrebbero essere apportate ad APX
prima della commercializzazione definitiva dei primi processori che la implementeranno (sempre che non sia troppo tardi, ormai!).
Modifiche varie
Una modifica che suggerisco è quella di trattare le istruzioni condizionali come fanno altri processori che consentono di ignorarne totalmente gli effetti nel caso in cui la condizione specifica non venga soddisfatta. In questo modo anche l’implementazione nella pipeline d’esecuzione diventa più semplice (si esegue soltanto il commit o il retire dell’istruzione).
Attualmente, invece, se la condizione non si verifica l’argomento di destinazione viene in ogni caso azzerato (CFCMOVcc
) se si tratti di un registro (mentre rimane inalterato altrimenti). La versione originale di CMOVcc
ha anche la pecca di generare eccezioni se non è possibile accedere alla locazione di memoria che referenzia, anche quando la condizione è falsa, ma fortunatamente APX
ne fornisce una (CFCMOVcc
) che sopprime le eccezioni i questi casi.
Tutte queste singole differenze e comportamenti diversi a seconda dell’istruzione non giovano né al decoder che deve decodificarle né al backend che deve eseguirle. Lo stesso si verifica quando soltanto ad alcune istruzioni viene data la possibilità di poter sopprimere la generazione dei flag, mentre ad altre no. Ciò si traduce in una maggior complessità implementativa, anche a carico dei compilatori (che devono tenere conto e gestire tutte questi casi speciali).
Modifiche a REX2
(per aggiungere NF
)
Quindi la prossima concreta, nonché estremamente semplice, modifica sarebbe quella di dare la possibilità di utilizzare il bit NF
(No Flags
) a tutte le istruzioni “promosse” da questa nuova estensione, anziché soltanto ad alcune.
In realtà tutti i miglioramenti proposti in quest’articolo comportano la completa rimozione del concetto di “promozione” (che attualmente avviene soltanto per alcune istruzioni. Il che ha portato alla creazione della mappa 4
usando il prefisso EVEX
, come abbiamo già visto nel primo articolo), poiché l’idea è quella di consentire a tutte le istruzioni general-purpose di usufruire delle nuove funzionalità introdotte con APX
.
Per arrivare allo scopo (dando, al contempo, una bella mano alla densità di codice) si rende necessaria una banale modifica al prefisso REX2
, che attualmente ha la seguente struttura:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
REX2 (2-byte REX) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0xD5) | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | |
1 | M0 | R4 | X4 | B4 | W | R3 | X3 | B3 |
Il quale, aggiungendo il bit NF
per segnalare l’eventuale soppressione della generazione dei flag, diventa:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
New REX2 (2-byte REX) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0xD4, 0xD5) | 1 | 1 | 0 | 1 | 0 | 1 | 0 | M0 | |
1 | NF | R4 | X4 | B4 | W | R3 | X3 | B3 |
Adesso non si utilizza soltanto l’opcode (D5
in esadecimale) della vecchia istruzione AAD
(soppressa da x64
in modalità a 64 bit), ma anche quello di AAM
(D4
), i quali consentono di poter impostare NF
(nell’MSB: il bit più significativo del secondo byte), per l’appunto, senza alcun’altra penalizzazione a parte quella di usare REX2
che, però, occupa soltanto due byte (al contrario di EVEX
dove, invece, ne sarebbero necessari ben quattro!).
Il perché NF
abbia preso il posto di M0
rispetto all’originale in REX2
si vedrà meglio più avanti con gli altri prefissi, ma anticipo che serve a mantenere esattamente lo stesso formato del secondo byte, ovunque. Mentre per la mappa da selezionare esistono delle differenze, a seconda del prefisso (ma è l’unica variante).
Nuovo prefisso REX3
(per aggiungere la condizione)
Sulla stessa scia e come precedentemente suggerito, si potrebbe applicare una condizione a tutte le istruzioni general-purpose. Dando, quindi, loro la possibilità di poter essere totalmente ignorate nel caso non venisse soddisfatta e senza alcun effetto collaterale (anche questo già ampiamente spiegato in precedenza).
Questa modifica è estremamente importante proprio per dare man forte alla dichiarazione di Intel presente nella presentazione di APX
, la quale afferma che le pipeline dei processori stanno diventando sempre più lunghe (e larghe) col passare del tempo e, quindi, più suscettibili a perdite prestazionali quando la previsione dei salti condizionati fallisca.
La soluzione che propongo, allo scopo, è quella di introdurre un nuovo prefisso, REX3
, molto simile a REX2
, ma con l’aggiunta di un byte in cui è possibile specificare la condizione da soddisfare obbligatoriamente al fine di approvare l’esecuzione della relativa istruzione. Il formato del nuovo prefisso è il seguente:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
REX3 (3-byte REX) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0x1F) | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | |
1 | NF | R4 | X4 | B4 | W | R3 | X3 | B3 | |
2 | 0 | 0 | 0 | M0 | SC3 | SC2 | SC1 | SC0 |
dove, come abbiamo già visto nel primo articolo che esponeva il formato di tutti i prefissi aggiunti oppure modificati da APX
,
sono quattro bit che rappresentano il codice (modificato, escludendo il test per il bit di parità SC3..SC0
P
) della condizione che viene utilizzata nei salti condizionati. Mentre NF
è il bit di No Flags
che abbiamo già visto sopra col nuovo prefisso REX2
.
I tre bit a 0
nel terzo byte, che si trovano prima di M0
, lasciano spazio per eventuali altre mappe da aggiungere (anche se, usandoli tutti per questo scopo, 16 sarebbero troppe) e/o per abilitare, in eventuali future estensioni, altre funzionalità.
Come si può vedere, questo nuovo prefisso (per il quale ho impiegato l’opcode 1F
, che corrisponde alla vecchia istruzione legacy POP DS
) è abbastanza semplice, flessibile, e più facile da implementare rispetto ad EVEX
, oltre al fatto che ha pure il non trascurabile vantaggio di occupare un byte in meno rispetto a quest’ultimo e, quindi, di mitigare l’impatto sulla densità di codice.
Sfruttando REX3
è anche possibile (re)implementare le nuove istruzioni CCMP
e CTEST
sfruttando gli opcode 70-7F
(mappa 0
: le classiche istruzioni di salto condizionato con offset di 8 bit per il salto) per la prima e 80-8F
(mappa 1
: sono i meno famosi salti condizionati con un offset di 16 o 32 bit) per la seconda. I primi 4 bit (quelli meno significativi) saranno usati per specificare il valore dei campi OF
, SF
, ZF
e CF
, da copiare nei rispettivi flag nel caso in cui la condizione in REX3
non venisse soddisfatta.
In questo caso il formato dell’ istruzione per CCMP
diventa il seguente:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
REX3 (3-byte REX) for CCMP | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0x1F) | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | |
1 | NF | R4 | X4 | B4 | W | R3 | X3 | B3 | |
2 | 0 | 0 | 0 | 0 | SC3 | SC2 | SC1 | SC0 | |
3 | 0 | 1 | 1 | 1 | OF | SF | ZF | CF |
Mentre per CTEST
:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
REX3 (3-byte REX) for CTEST | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0x1F) | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | |
1 | NF | R4 | X4 | B4 | W | R3 | X3 | B3 | |
2 | 0 | 0 | 0 | 1 | SC3 | SC2 | SC1 | SC0 | |
3 | 1 | 0 | 0 | 0 | OF | SF | ZF | CF |
La scelta di riutilizzare gli opcode delle istruzioni di salto condizionato è certamente la migliore, perché trasformare (tramite il nuovo REX3
) in condizionali istruzioni che già di per sé lo sono non avrebbe alcun senso. Tanto vale, allora, riutilizzarle, sfruttando i 4 bit della condizione per memorizzare, invece, i valori di OF
, SF
, ZF
e CF
.
Si tratta di un’implementazione molto semplice, come si può vedere, che richiede un paio di banali confronti in presenza del nuovo prefisso REX3
per verificare se si trovi davanti al caso speciale di queste due nuove istruzioni e che ha, inoltre, il vantaggio di occupare un byte in meno rispetto all’attuale soluzione che fa uso di EVEX
, migliorando, quindi, la densità di codice.
Modifiche a VEX3
(per i nuovi registri)
A tal proposito si potrebbe migliorare in maniera triviale la densità di codice anche per le istruzioni (AVX
, AVX-2
) che facciano uso del prefisso VEX3
, nel caso in cui si rendesse necessario accedere ai 16 registri general-purpose che APX
ha aggiunto, senza per questo dover ricorrere forzatamente al più lungo (che occupa un byte in più) e complicato EVEX
. Attualmente VEX3
ha il seguente formato:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
VEX3 (3-byte VEX) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0xC4) | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | |
1 | R̅ | X̅ | B̅ | m4 | m3 | m2 | m1 | m0 | |
2 | W | v̅3 | v̅2 | v̅1 | v̅0 | L | p1 | p0 |
mentre con la mia proposta diventerebbe:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
New VEX3 (3-byte VEX) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0xC4) | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | |
1 | R̅3 | X̅3 | B̅3 | R4 | X4 | B4 | m1 | m0 | |
2 | W | v̅3 | v̅2 | v̅1 | v̅0 | L | p1 | p0 |
Riutilizzando, quindi, i bit m4..m2
per aggiungere i 3 bit necessari per poter specificare i nuovi registri. In questo modo le mappe degli opcode selezionabili scenderebbero da 32 a 4 soltanto, ma non sarebbe un grosso problema per un paio di motivi.
Il primo è che attualmente ci sono soltanto quattro mappe per tutte le istruzioni (e c’è ancora spazio a disposizione per aggiungerne altre), per cui non ne verrebbe a mancare nessuna. Il secondo è che la tendenza attuale è quella di utilizzare AVX-512
per estendere il set di istruzioni SIMD
, che fa sempre uso del prefisso EVEX
(il quale supporta fino a 8 mappe. Per cui c’è ampio spazio per aggiungere un altro migliaio di istruzioni).
Nuovi prefissi REXM0
e REXM1
per eliminare EVEX
Con un approccio simile, ma copiando quanto già fatto col prefisso REX3
che ho proposto poco sopra, si potrebbe evitare del tutto di utilizzare EVEX
per poter “promuovere” le istruzioni da binarie a ternarie, e da unarie a binarie, che EVEX
rende possibile grazie al nuovo bit ND
(che, posto a 1
, abilita questa nuova funzionalità) e al campo v̅4..v̅0
che consente di specificare il registro da utilizzare per memorizzare il risultato dell’operazione.
Si tratterebbe, in questo caso, di riutilizzare alcuni opcode che x64
ha liberato (rimuovendo alcune istruzioni legacy x86
) per aggiungere i seguenti due prefissi:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
REXM0 (3-byte REX with NDD, for map 0) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0x06, 0x16) | 0 | 0 | 0 | NDD4 | 0 | 1 | 1 | 0 | |
1 | NF | R4 | X4 | B4 | W | R3 | X3 | B3 | |
2 | NDD3 | NDD2 | NDD1 | NDD0 | SC3 | SC2 | SC1 | SC0 | |
REXM1 (3-byte REX with NDD, for map 1) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0x0E, 0x1E) | 0 | 0 | 0 | NDD4 | 1 | 1 | 1 | 0 | |
1 | NF | R4 | X4 | B4 | W | R3 | X3 | B3 | |
2 | NDD3 | NDD2 | NDD1 | NDD0 | SC3 | SC2 | SC1 | SC0 |
Come si può vedere, i due nuovi prefissi (che utilizzano gli opcode 06
, 16
, 0E
e 1E
, corrispondenti alle vecchie istruzioni PUSH ES
, PUSH SS
, PUSH CS
, PUSH DS
) REXM0
e REXM1
sono molto simili a REX3
, ma con qualche leggera differenza.
Intanto è possibile specificare il registro destinazione (NDD
) tramite i nuovi bit NDD4..NDD0
(senza dover impostare il bit ND
, il quale risulta implicitamente specificato). Poi, il bit M0
è sparito per far posto a
, poiché adesso la mappa NDD0
0
oppure la 1
viene selezionata usando l’apposito prefisso (REXM0
per la mappa 0
e REXM1
per la mappa 1
). In maniera similare, e se dovesse servire, altri prefissi potrebbero essere aggiunti per supportare nuove mappe (ci sono ancora abbastanza opcode di istruzioni legacy che sono libere in x64
).
Va sottolineato che questi due prefissi non necessitano di implementare anche le nuove istruzioni CCMP
e CTEST
, poiché in questo caso non è serve utilizzare il nuovo registro di destinazione (non c’è alcun risultato da memorizzare: si tratta soltanto di istruzioni che alterano i flag). E’ sufficiente, quindi, la loro implementazione sfruttando soltanto REX3
, come già esposto sopra.
Questi due nuovi prefissi sono più corti (di un byte) rispetto a EVEX
, consentendo quindi di limitare i danni alla densità di codice dovuti all’impiego di prefissi così lunghi, ma hanno anche l’ulteriore vantaggio di rendere condizionale qualunque istruzione general-purpose che sia stata estesa a ternaria o binaria.
Ad esempio:
; Add
123456789
0 to the 64-bit value from memory and save it to RAXif the zero flag (Z) is set.
ADD.Z RAX,[RBX + RCX * 8 + 1234],1234567890
il cui funzionamento nonché potenziale dovrebbe essere intelligibile, ma col particolare da sottolineare che l’istruzione non genererebbe alcuna eccezione nel caso in cui la condizione non fosse verificata e l’elemento in memoria risultasse inaccessibile.
Inoltre, e per chiudere, REXM0
e REXM1
sono anche molto più semplici da implementare (il meccanismo è simile a REX2
e REX3,
che a loro volta sono simili a REX
) rispetto all’enorme complicazione del nuovo prefisso EVEX
.
Modifiche a EVEX
(per i nuovi registri)
Il quale adesso, ed essendo divenuto del tutto inutile per la “promozione” delle istruzioni general-purpose, richiede soltanto la banale aggiunta dei 3 bit per indirizzare i nuovi registri APX
, come già proposto per VEX3
. Dunque il suo nuovo formato sarà questo:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
---|---|---|---|---|---|---|---|---|---|
Byte 0 (62h) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | |
Byte 1 (P0) | R̅3 | X̅3 | B̅3 | R̅4 | B4 | m2 | m1 | m0 | P[7:0] |
Byte 2 (P1) | W | v̅3 | v̅2 | v̅1 | v̅0 | X̅4 | p1 | p0 | P[15:8] |
Byte 3 (P2) | z | L’ | L | b | v̅4 | a2 | a1 | a0 | P[23:16] |
e continuerebbe a funzionare esattamente come adesso: esclusivamente per le istruzioni di AVX-512
.
Sintesi delle modifiche proposte
Arrivati in chiusura penso sia opportuno ricapitolare i benefici derivanti dalle modifiche proposte per APX
:
- implementazione semplificata (e, conseguentemente, minor transistor & consumi);
- minor impatto sulla densità di codice (dal 25% al 50% di spazio occupato in meno per i nuovi prefissi, rispetto all’uso di
EVEX
, sia per le istruzioni general-purpose sia per quelleAVX/VEX3
), che a sua volta si traduce in minori consumi (meno pressione sulle cache e, in generale, su tutta la gerarchia della memoria); - tutte le istruzioni general-purpose che modifichino i flag possono sopprimerne la generazione (l’uso di
NF
diventa ortogonale); - tutte le istruzioni general-purpose diventano condizionali (con semplificazione sia dei compilatori sia della pipeline d’esecuzione che, adesso, deve soltanto convalidarne o meno l’esecuzione).
Dovrebbero essere evidenti i vantaggi di queste soluzioni, a parità di nuove funzionalità messe a disposizione, con la non trascurabile possibilità di eseguire in maniera condizionata tutte le istruzioni general-purpose (una nuova caratteristica che, quindi, si va ad aggiungere a quanto offerto da APX
).
E’ da notare, infine, che i nuovi prefissi sono stati pensati per utilizzare tutte le innovazioni in maniera incrementale. Il nuovo REX2
offre, di base, l’accesso ai nuovi registri e alla soppressione della generazione dei flag (NF
). A ciò REX3
aggiunge la possibilità di specificare la condizione per l’esecuzione dell’istruzione. REXM0
e REXM1
aggiungono, a questo, il nuovo registro di destinazione (NDD
). Il tutto in maniera semplice e “compiler-friendly“.
Il prossimo articolo sarà l’ultimo e riporterà le conclusioni riguardo APX
.