A poca distanza della proposta di X86-S
, Intel presenta la più grande novità in assoluto fin dai tempi di AVX-512
— e anche oltre: APX
, la sua nuova (vera) architettura (ISA), ma esclusivamente a 64 bit (non è e in ogni caso non sarebbe potuta esser supportata in modalità a 32 bit). Questa serie di articoli si prefigge il compito di analizzarne le innovazioni, criticità e fornire spunti di riflessioni.
Intanto e per essere precisi (e per com’è stata introdotta), non si può parlare formalmente di una nuova architettura, in quanto si tratta di aggiunte a x64
che sono state effettuate tramite la nuova estensione dell’ISA
che si chiama APX
, per l’appunto. Estensione che, quindi, sfrutta come base la modalità a 64 bit introdotta nel 2003 da AMD, a cui vengono aggiunte le seguenti caratteristiche:
- 16 nuovi registri general-purpose;
- diverse istruzioni sono disponibili in un nuovo formato che utilizza tre operandi anziché i canonici due, oppure due per quelle che posseggono soltanto uno;
- nuove istruzioni condizionali;
- istruzione di salto a indirizzi assoluti (a 64 bit, ovviamente).
C’eravamo già abituati da tantissimo tempo all’aggiunta di nuove istruzioni in tutte le salse e negli ambiti più disparati, e il tutto si andava ad aggiungere in maniera trasparente all’esistente, ma mettere a disposizione nuovi registri è una modifica estremamente significativa e impattante, tale da permettermi di parlare effettivamente di nuova architettura.
Ciò perché, sebbene il codice facente uso di APX
funzioni e si possa mischiare tranquillamente con qualunque codice x64
, mettere a disposizione più code path (come, ad esempio, rende possibile il compilatore Intel) nello stesso eseguibile per supportare più scenari (a seconda del processore su cui il codice giri) ne farebbe aumentare le dimensioni anche in maniera significativa.
Finché si tratta di poche parti critiche che hanno bisogno di appositi code path per farle girare al meglio a seconda degli specifici processori (parliamo, più precisamente, di specifiche microarchitetture), è una scelta che paga e che non fa lievitare eccessivamente le dimensioni dei binari, ma la portata di tutte le innovazioni di APX
è tale ed è talmente pervasiva in quasi ogni aspetto del codice, che è difficile pensare che si possa continuare a seguire quest’approccio (sebbene teoricamente e praticamente fattibile).
La mia aspettativa è, pertanto, che si assisterà alla generazione di eseguibili appositamente dedicati ad APX
, senza alcun code path per il normale codice x64
(il quale sarà relegato a binari equivalenti), in modo non soltanto da massimizzare le prestazioni, ma anche contenere le dimensioni del codice complessivamente (anche su disco). Questo è anche il motivo per cui preferisco pensare ad APX
come a nuova architettura anziché come a un’estensione come quelle che sono state introdotte finora.
Il prefisso REX2
per accedere ai nuovi registri
I nuovi registri non possono essere normalmente indirizzabili, per cui all’ISA sono state apportate alcune modifiche allo scopo, similmente a quanto realizzato da AMD per x64
con l’introduzione del prefisso REX:
Byte | Bit | ||||||||
---|---|---|---|---|---|---|---|---|---|
REX | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 | 0 | 1 | 0 | 0 | W | R | X | B |
Spicca, dunque, l’introduzione del nuovo prefisso REX2
:
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 |
questa volta incentrato sulla necessità di adoperare ulteriori bit che consentono di accedere al nuovo “banco” di registri. Come si può subito notare, R3
, X3
e B3
sono gli equivalenti di R
, X
e B
e servono per specificare il quarto bit del registro (specificato nell’opcode stesso o nell’operando in memoria), indice e base rispettivamente (se c’è un operando in memoria), mentre R4
, X4
e B4
fanno la stessa cosa, ma per il quinto bit (5 bit = 32 valori possibili = 32 registri indirizzabili).
Appare, però, un ulteriore bit, M0
, il quale serve a specificare quale “mappa di opcode” utilizzare: la 0
oppure la 1
. Qui serve una piccola digressione sul modo in cui siano stati definiti / mappati gli opcode delle istruzioni x86
e x64
. In questi processori gli opcode sono definiti da sequenze di byte (altre architetture possono utilizzare word come “misura base”, costituite da 2 o più byte).
Tralasciando per il momento il discorso sui prefissi e semplificando molto la trattazione, un’istruzione 8086
(che è stato il capostipite di quest’ISA) utilizza il primo byte per definire fino a un massimo di 256 opcode che corrispondono (molto) rozzamente a 256 istruzioni. L’8086
non ha usato tutte e 256 le possibilità, ma soltanto un sottoinsieme, lasciando alcune configurazioni libere che, però, col tempo sono state utilizzate da nuove istruzioni (o prefissi) che sono state via via aggiunte. L’insieme delle istruzioni che appartengono a queste 256 configurazioni specificate dal primo byte viene chiamato “mappa 0
“.
A un certo punto tale mappa ha esaurito tutte le possibilità, e per aggiungere ulteriori istruzioni Intel ha pensato bene di riciclare un’istruzione che per 8086
era del tutto inutile (in realtà pericolosa: POP CS
; il cui opcode è 0F
in esadecimale) e riutilizzarla per creare una nuova mappa di opcode (la mappa 1
). Aprendo, quindi, alla possibilità di definire altre 256 possibili istruzioni (quelle che hanno 0F
come primo byte e il cui secondo byte specifica il nuovo opcode), con l’unica penalizzazione che, in questo caso, le istruzioni sono diventate più lunghe di un byte (perché il primo è sempre 0F
).
Com’è facile intuire, anche la mappa 1
è andata via via esaurendosi aggiungendo istruzioni, per cui Intel ne ha utilizzato un paio di configurazioni (0F 38
e 0F 3A
) per creare un altro paio di mappe (2
e 3
). Queste istruzioni, quindi, sono ancora più lunghe (causa primi due byte che hanno uno di quei due valori).
Il nuovo bit M0
consente, quindi, di poter selezionare soltanto una delle prime due mappe, ma non le altre due. Il che non è nemmeno un grosso problema, perché buona parte delle istruzioni “general-purpose” (chiamate anche legacy nel documento di Intel che racchiude le specifiche complete di APX
) sono racchiuse nelle prime due mappe.
Le altre due si possono in ogni caso selezionare tramite i prefissi VEX
(VEX2
e VEX3
introdotti da Intel con le estensioni AVX
):
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 | |
VEX2 (2-byte VEX) | |||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
0 (0xC5) | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | |
1 | R̅ | v̅3 | v̅2 | v̅1 | v̅0 | L | p1 | p0 |
oppure col prefisso EVEX
(introdotto da Intel con le estensioni AVX-512
):
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
---|---|---|---|---|---|---|---|---|---|
Byte 0 (62h) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | |
Byte 1 (P0) | R̅ | X̅ | B̅ | R̅’ | 0 | 0 | m1 | m0 | P[7:0] |
Byte 2 (P1) | W | v̅3 | v̅2 | v̅1 | v̅0 | 1 | p1 | p0 | P[15:8] |
Byte 3 (P2) | z | L’ | L | b | V̅’ | a2 | a1 | a0 | P[23:16] |
VEX2
consente di poter selezionare soltanto la seconda mappa (0F
, che è implicita), mentre VEX3
fino a ben 32 tramite i bit m4..m0
, e infine EVEX
permette di selezionare tutte e quattro le mappe tramite m1..m0
.
VEX2
è un prefisso di due byte. VEX3
di tre byte. EVEX
, invece, è di ben 4 byte. Per cui risulta evidente che la lunghezza delle istruzioni aumenterà, anche notevolmente, in base al prefisso utilizzato.
Modifiche al prefisso EVEX
Finora il prefisso REX2
ha introdotto esclusivamente la possibilità di poter selezionare i 16 nuovi registri general-purpose, ma Intel con APX
è andata ben oltre, estendendo il funzionamento di una parte delle istruzioni (non soltanto alcune generali, ma anche alcune AVX
) e dando loro nuove possibilità che in precedenza richiedevano più istruzioni per emularne il comportamento. Per far ciò ha esteso, a sua volta, il prefisso EVEX
sfruttando alcuni bit (che prima erano inutilizzati). In origine il suo formato era quello riportato nella precedente tabella, mentre adesso, per supportare i nuovi registri general-purpose, è diventato:
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] |
Quindi sono semplicemente stati aggiunti R4
, X4
e B4
(i primi due in versione negata per questioni tecniche che non sto qui a ripetere) per poter impostare il quinto di bit del rispettivo registro.
Anche alcune istruzioni VEX
sono state aggiornate, ma non avendo la possibilità di specificare bit addizionali nei prefissi VEX2
e VEX3
, Intel ha pensato di sfruttare sempre EVEX
per estenderne il funzionamento in questi casi:
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) | 0 | 0 | L | 0 | v̅4 | NF | 0 | 0 | P[23:16] |
L’unica differenza sostanziale (a parte poter accedere ai nuovi registri) rispetto ai prefissi VEX
è fornita dal nuovo bit NF
, che consente di sopprimere la generazione dei flag per alcune istruzioni che, in origine, ne prevedevano la generazione in base al risultato dell’operazione. Di questo ne parlerò meglio in un’apposita sezione del prossimo articolo.
Per quelle generali, invece, ha apportato dei cambiamenti più consistenti:
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 | 1 | 0 | 0 | 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) | 0 | 0 | 0 | ND | v̅4 | NF | 0 | 0 | P[23:16] |
Di NF
ne ho già parlato, ma ad esso si va, adesso, ad aggiungere il bit ND
, il quale consente di trasformare un’istruzione binaria (un operando sorgente e uno di destinazione che funge anche da seconda sorgente) in ternaria oppure una unaria (l’unico operando è usato sia come sorgente sia come destinazione) in binaria, secondo alcune precise logiche che verranno anch’esse spiegate in un’altra sezione del prossimo articolo.
E’ da notare, però, che in questo caso i bit m2..m0
(ad EVEX
adesso è stato aggiunto m2
per portare le mappe selezionabili da 4 a 8) abbiano assunto un valore ben preciso: 100
(in binario). EVEX
utilizza questo campo per poter specificare quale mappa, fra le otto possibili, utilizzare per il byte di opcode che segue tale prefisso. Abbiamo visto che su x64
esistono quattro mappe (da 0
a 3
) che definiscono i corrispondenti insiemi di istruzioni, ma 100
equivale a una nuova mappa: la 4
!
Il motivo è presto spiegato: non tutte le istruzioni generali sono state estese con questi nuovi meccanismi (NF
e ND
), ma soltanto alcune lo sono. Intel ha pensato, per non complicare ulteriormente le cose, di raccogliere tutte le istruzioni “promuovibili” e infilarle in questa nuova mappa. Dunque le nuove meraviglie sono riservate esclusivamente alle istruzioni presenti nella mappa 4
, mentre tutte le altre istruzioni possono soltanto accedere ai nuovi registri (tramite REX2
) e non possono sfruttare altro.
Intel, infine, ha cambiato ancora quest’ultimo formato per poter introdurre alcune nuove istruzioni condizionali:
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 | 1 | 0 | 0 | P[7:0] |
Byte 2 (P1) | W | OF | SF | ZF | CF | X̅4 | p1 | p0 | P[15:8] |
Byte 3 (P2) | 0 | 0 | 0 | ND=1 | SC3 | SC2 | SC1 | SC0 | P[23:16] |
I campi OF
, SF
, ZF
e CF
sono proprio gli stessi bit presenti nel registro dei flag, mentre SC3..SC0
sono quattro bit che definiscono il codice (ma con alcune modifiche) che viene normalmente utilizzato nei salti condizionati per indicare quando effettuare il salto. Un’apposita sezione del prossimo articolo tratterà di queste particolari nuove istruzioni condizionali.
Il prossimo articolo affronterà, come già anticipato, più in dettaglio le innovazioni di APX
e l’impatto che hanno in scenari reali / comuni.