APX: la nuova architettura di Intel – 7 – Miglioramenti possibili

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:

ByteBit
REX2 (2-byte REX)
76543210
0 (0xD5)11010101
1M0R4X4B4WR3X3B3

Il quale, aggiungendo il bit NF per segnalare l’eventuale soppressione della generazione dei flag, diventa:

ByteBit
New REX2 (2-byte REX)
76543210
0 (0xD4, 0xD5)1101010M0
1NFR4X4B4WR3X3B3

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:

ByteBit
REX3 (3-byte REX)
76543210
0 (0x1F)00011111
1NFR4X4B4WR3X3B3
2000M0SC3SC2SC1SC0

dove, come abbiamo già visto nel primo articolo che esponeva il formato di tutti i prefissi aggiunti oppure modificati da APX, SC3..SC0 sono quattro bit che rappresentano il codice (modificato, escludendo il test per il bit di parità 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:

ByteBit
REX3 (3-byte REX) for CCMP
76543210
0 (0x1F)00011111
1NFR4X4B4WR3X3B3
20000SC3SC2SC1SC0
30111OFSFZFCF

Mentre per CTEST:

ByteBit
REX3 (3-byte REX) for CTEST
76543210
0 (0x1F)00011111
1NFR4X4B4WR3X3B3
20001SC3SC2SC1SC0
31000OFSFZFCF

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:

ByteBit
VEX3 (3-byte VEX)
76543210
0 (0xC4)11000100
1m4m3m2m1m0
2W3210Lp1p0

mentre con la mia proposta diventerebbe:

ByteBit
New VEX3 (3-byte VEX)
76543210
0 (0xC4)11000100
1333R4X4B4m1m0
2W3210Lp1p0

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 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:

ByteBit
REXM0 (3-byte REX with NDD, for map 0)
76543210
0 (0x06, 0x16)000NDD40110
1NFR4X4B4WR3X3B3
2NDD3NDD2NDD1NDD0SC3SC2SC1SC0
REXM1 (3-byte REX with NDD, for map 1)
76543210
0 (0x0E, 0x1E)000NDD41110
1NFR4X4B4WR3X3B3
2NDD3NDD2NDD1NDD0SC3SC2SC1SC0

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 NDD0, poiché adesso la mappa 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 1234567890 to the 64-bit value from memory and save it to RAX if 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:

76543210
Byte 0 (62h)01100010
Byte 1 (P0)3334B4m2m1m0P[7:0]
Byte 2 (P1)W32104p1p0P[15:8]
Byte 3 (P2)zL’Lb4a2a1a0P[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 quelle AVX/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.

Press ESC to close