APX: la nuova architettura di Intel – 4 – Vantaggi & pecche

Finito di passare in rassegna tutte le novità di APX (con l’ultimo articolo che analizzava le nuove istruzioni), è arrivato il momento di un bilancio sui vantaggi (di cui abbiamo, comunque, avuto già una sostanziosa panoramica) e le pecche (non è, infatti, esente da difetti o mancanze).

Vantaggi di APX

Cominciamo, quindi, partendo dai vantaggi che APX apporta alla vetusta (ma sempre viva!) architettura x64 (che a sua volta ha esteso l’ancora più antica nonché celebre x86). Compito che risulta alquanto facile perché, a mio modesto avviso, sono quattro (più una che tratterò nell’ultima parte) le innovazioni degne di merito, anche se sono soltanto le prime due quelle più importanti e incisive.

La prima è l’estensione delle più comuni istruzioni binarie in ternarie, e unarie in binarie. Con APX abbiamo, finalmente!, la possibilità di utilizzare tre operandi oppure due, nei rispettivi casi, che consentono di evitare l’utilizzo di più istruzioni per effettuare la stessa operazione e, in molti casi, anche senza utilizzare un registro addizionale per qualche operazione intermedia.

Un esempio proveniente sempre da FFMPEG (x64):

MOV R8D, ECX
XOR R8D, 0x1

che con APX diventerebbe, invece:

XOR R8D, ECX, 0x1
Oppure e ancora meglio:

MOV EBX, [RBP+0x30]
AND EBX, 0x80

che con APX diventerebbe:

AND EBX, [RBP+0x30], 0x80
Dove l’operazione ternaria si combina con la possibilità, per uno dei suoi due argomenti, di referenziare direttamente la sorgente dati in memoria.

I benefici prestazionali sono, quindi, immediatamente palpabili e non necessitano di ulteriori lodi, se non quella che questo nuovo meccanismo di promozione di alcune istruzioni ha dalla sua anche l’ulteriore vantaggio non soltanto di poter utilizzare un operando in memoria come sorgente (cosa alquanto rara, se non del tutto assente, in svariate altre architetture) ma, per giunta, indifferentemente come prima o seconda sorgente (se la particolare istruzione lo consentisse).

Intel afferma che, con APX, vengono eseguite circa il 10% in meno di istruzioni rispetto a x64 su dati preliminari usando il famosissimo benchmark SPEC2017, e non ho dubbi che il maggior contributo qui provenga proprio dalla “promozione” di queste istruzioni. Ciò rappresenta un elemento distintivo dei processori CISC, il quale si arricchisce ulteriormente in questo caso e che, parafrasando la nota cantante Madonna, mi fa scappare un: “CISCs do it better!”.

La seconda innovazione di notevole rilevanza è l’estensione dei registri general-purpose a 32 in totale: 16 registri in più sono decisamente comodi in diversi ambiti (mi vengono in mente macchine virtuali ed emulatori, in primis, ma anche compilatori / parser, database, ecc.).

Ed è proprio grazie ai 16 nuovi registri addizionali che Intel afferma di aver ridotto del 10% le operazioni di load e di oltre il 20% quelle di store, anche qui con evidenti benefici prestazionali (si fa meno uso delle cache dati e, in generale, ne trae vantaggio l’intera gerarchia della memoria).

Il che, lasciatemelo dire, si scontra pesantemente con le dichiarazioni di AMD seguite all’introduzione della sua architettura x86-64/x64, le quali, come avevo già anticipato nel precedente articolo, affermavano che il passaggio da 16 a 32 registri avrebbe offerto scarsi vantaggi (non giustificando la maggior complessità implementativa). Non sappiamo, però, in che modo quest’azienda avesse pensato di aggiungere tutti questi registri, rispetto al meccanismo che ha, adesso, realizzato Intel con APX.

Interessante risulta anche l’aggiunta delle nuove istruzioni PUSH2 e POP2, le quali operano su due registri alla volta, il che consente quasi di dimezzare il numero delle usuali PUSH e POP presenti nel codice, come abbiamo già visto nell’articolo precedente, il quale ha mostrato anche una porzione di codice reale a dimostrare come si usino sequenze di PUSH e POP nel prologo e nell’epilogo di una routine (scenario non affatto inusuale). Anche qui parliamo di benefici a livello prestazionale (meno istruzioni eseguite).

Un po’ meno utile risulta, invece, la possibilità, per le istruzioni che godano della possibilità di essere “promosse”, di poter sopprimere la generazione dei flag, per quanto già discusso in dettaglio in precedenza. Gli scenari non sono così comuni come quelli elencati per gli altri punti qui sopra, ma sono abbastanza rilevanti da meritare l’introduzione di questa caratteristica (specialmente se il costo implementativo fosse irrisorio).

Pecche di APX

Passando alle critiche e alle problematiche (non è tutto meraviglioso) e ricongiungendomi immediatamente all’ultima caratteristica menzionata proprio qui sopra, non capisco per quale assurdo motivo la soppressione della generazione dei flag sia appannaggio soltanto di alcune istruzioni (parliamo, in ogni caso, sempre di quelle “promosse”) e non di tutte: non ha alcun senso!

Un esempio sono le istruzioni ADC e SBB (addizione e sottrazione facenti uso del flag di carry): il bit NF non è utilizzabile (dev’essere lasciato obbligatoriamente a 0), mentre le tradizionali ADD e SUB (che non usano il carry) lo possono abilitare. Ovviamente non sono gli unici casi, ma ci sono parecchie istruzioni “promosse” e che normalmente generano flag, per le quali non è possibile abilitarne la soppressione.

Continuando sulla stessa scia, le pur utili istruzioni PUSH2 e POP2 potevano anche essere evitate, delegando alla sola microarchitettura il compito di individuare le coppie da “unire in matrimonio” sfruttando il già presente meccanismo di macro-op fusion, oltre al fatto che sono decisamente più lunghe (ma della densità di codice ne parlerò più approfonditamente nel prossimo articolo).

E’ certamente più complicato da realizzare a livello di implementazione, ma sarebbe anche stato trasparente rispetto alle applicazioni già esistenti (e, quindi, sfruttabile persino su codice a 32 bit / x86, il quale abbonda ancor più di sequenze di PUSH e POP causa ABI che è stack-based anziché register-based).

Francamente non vedo, poi, così interessanti le nuove istruzioni basate su CMOVcc. A parte l’estensione ternaria (che, comunque, è naturale / ovvia, e rientra nel caso dell’estensione delle istruzioni “promosse” da binarie a ternarie. Quindi avrebbe comunque tratto godimento da questa nuova funzionalità), risultano di poca utilità le CFCMOVcc.

Sì, il poter sopprimere, eventualmente, le eccezioni nel caso di condizione insoddisfatta (come già chiarito nel precedente articolo) è un sacrosanto miglioramento, ma in realtà tutte le istruzioni condizionali avrebbero dovuto esser implementate in questo modo, fin dalla loro introduzione col Pentium Pro! La critica, quindi, sta proprio qui: un meccanismo del genere avrebbe dovuto essere esteso a molte più istruzioni anziché essere relegato alla sola MOV (ne parlerò meglio nel futuro articolo sui possibili miglioramenti ad APX).

Similmente, il fatto che soltanto alcune istruzioni possano essere “promosse” e, quindi, sfruttare le nuove e interessanti caratteristiche che consentono di espanderne gli utilizzi, è una grossa limitazione che complica sia la scrittura dei compilatori (i quali prediligono set di istruzioni ortogonali) sia l’implementazione (maggiori dettagli un futuro articolo proprio in merito).

Oltre al fatto che, per com’è stata concepita, la nuova mappa 4 (in cui risiedono queste istruzioni promosse, come già illustrato nel primo articolo) in futuro potrebbe esaurire il numero di istruzioni codificate in essa, costringendo a crearne un’altra e a complicare ulteriormente l’implementazione dell’architettura. In questo caso, tra l’altro, non si potrà più fare uso del prefisso REX2 (perché riesce a mappare soltanto gli opcode delle mappe 0 e 1), costringendo a usare sempre EVEX e pagarne i maggiori costi in termini di minor densità di codice.

E poi ci sono CCMP e CTEST!

Le nuove istruzioni CCMP e CTEST meritano, infine, una trattazione a parte, perché presentano vantaggi e pecche, per cui ho preferito dedicare un’apposita sezione anziché frammentarne la discussione nelle altre due.

Partiamo dai vantaggi, che non sembrano affatto evidenti, poiché il loro meccanismo di funzionamento appare, in effetti, piuttosto contorto e, a prima vista, addirittura del tutto inutile. Non è facile, infatti, aver chiaro in mente a cosa diavolo possano servire e in quale scenario reale possano portare dei tangibili vantaggi.

L’obiettivo dichiarato esplicitamente dall’azienda (nella presentazione di APX) è quello di ridurre il numero di salti condizionati sfruttando, al loro posto, istruzioni condizionali, in modo da cercare di mitigare il più possibile le problematiche relative alle pipeline, che col tempo diventano sempre più lunghe (e, quindi, le istruzioni di salto condizionate, la cui previsione da parte del predittore è fallita, comportano un prezzo molto elevato da pagare).

Intel s’era già portata avanti parecchi anni fa con l’introduzione di SETcc e CMOVcc (di cui ho già ampiamente discusso), sebbene rappresentino un timidissimo approccio paragonato a quanto offerto da altre architetture, e in particolare da ARM, la quale consente di eseguire in maniera condizionale qualunque sua istruzione (il che ne ha fatto uno dei suoi elementi distintivi; ma non è la sola ISA che funziona così, sebbene sia la più nota).

Ciò che ha lasciato estremamente meravigliati è l’aver scoperto che per la sua nuova architettura a 64 bit, chiamata AArch64 o ARM64, ARM abbia deciso di rimuovere completamente l’esecuzione condizionale su tutte le istruzioni, ripiegando sullo stesso approccio seguito da Intel, cioè mettere a disposizione soltanto alcune istruzioni che vengano eseguite in maniera condizionata.

La cosa non sorprende più di tanto se teniamo conto del fatto che, avendo da codificare istruzioni (in opcode a 32 bit) che lavorino con un banco di ben 32 registri, ARM abbia dovuto fare di necessità virtù eliminando l’esecuzione condizionale e, quindi, potendo riusare quattro preziosi bit per modellare meglio tutto il resto. D’altra parte la calcolatrice non mente: istruzioni ternarie richiedono 3 x 5 = 15 bit, e usandone altri 4 per la condizione da controllare, alla fine ne sarebbero rimasti 32 – 15 – 4 = 13 bit, che possono sembrare molti per codificare gli opcode di tutte le istruzioni, ma si esauriscono in fretta.

Scusandomi per questa breve digressione e tornando ad APX, questa volta è stata Intel a copiare pari pari l’approccio di ARM con AArch64, e in particolare l’istruzione CCMP (che coincidenza!) che funziona esattamente allo stesso modo (una descrizione è stata fornita nel precedente articolo).

Il perché di questa scelta è molto semplice: grazie a quest’istruzione si può emulare il controllo di espressioni condizionali più complesse, facenti uso di operatori booleani AND oppure OR per concatenare le singole condizioni più semplici. La questione non è di immediata comprensione, ma fortunatamente c’è uno splendido articolo di Raymond Chen sul blog di Microsoft che spiega in dettaglio e con esempi tutte le istruzioni condizionali introdotte da ARM in AArch64.

Quello che è importante, in questo contesto, è che quanto possa sembrare un contorsionismo inutile si riveli, invece, non soltanto molto utile, ma anche estremamente efficiente (gli esempi nel citato blog sono più che eloquenti) in termini prestazionali, assolvendo pienamente allo scopo per cui è nata quest’istruzione.

Una trovata davvero geniale da parte degli ingegneri ARM, che meritano tutto il mio rispetto, e che Intel ha fatto benissimo a copiare, ma offrendo maggior flessibilità per il secondo argomento del confronto, che è in grado di referenziare un dato in memoria (mentre il primo argomento si deve trovare sempre in un registro).

Qui, però, nasce anche la prima rogna poiché, essendo in memoria, tale operando potrebbe generare un’eccezione. CCMP si comporta, quindi, esattamente come CMOVcc: viene generata in ogni caso un’eccezione anche nel caso in cui la condizione fallisca, quando l’operando referenziato stia in memoria e non fosse accessibile.

Una scelta che, ovviamente, rimane palesemente sbagliata, poiché il fondamento alla base della valutazione di espressioni condizionali che siano “short circuit” (scusate, ma l’equivalente in italiano non trova il mio gradimento) è che se la prima condizione fosse sufficiente per valutare il risultato dell’intera espressione, allora tutto ciò che segue (le altre condizioni da controllare) non avrebbe bisogno di essere processato (infatti non lo si fa) e si passerebbe direttamente al blocco di codice da eseguire.

La cosa paradossale sarebbe che, come già riportato in precedenza, Intel abbia introdotto le nuove istruzioni CFCMOVcc appositamente per supportare questa diffusissima valutazione delle espressioni condizionali (senza generare eccezioni se non fosse il caso), ma ha deciso di non farlo dove ce ne sarebbe stato più bisogno: con la nuova CCMP, la quale è stata introdotta proprio allo scopo di aiutare in queste situazioni! E’ una decisione che mi lascia assolutamente interdetto, poiché anch’essa totalmente priva di senso…

Ha pensato di introdurre, invece, una CTEST, i cui casi d’uso sono di gran lunga più rari rispetto alle diffusissime operazioni di confronto. Se proprio avesse voluto mantenere il funzionamento di CCMP così com’è, avrebbe potuto, invece, aggiungere una CFCCMP e avrebbe risolto il problema, esattamente come ha fatto con la CMOVcc.

Sempre su quest’aspetto c’è da dire che, pur con queste nuove aggiunte, il numero di istruzioni condizionali che x64 mette a disposizione rimane comunque esiguo. Basti fare un confronto con quelle di AArch64 (riportate tutte nel suddetto blog) per vedere come sfiguri terribilmente: ci sono soltanto SETcc, CMOVcc, CCMP e CTEST. Con APX Intel avrebbe potuto approfittarne e mettersi al passo, aggiungendone altre, oppure implementare un’altra soluzione (come verrà spiegato nel futuro articolo sui possibili miglioramenti).

Dulcis in fundo, per introdurre CCMP e CTEST ha deciso di complicare l’implementazione dei suoi processori, in quanto viene richiesta un’implementazione diversa del prefisso EVEX, appositamente ed esclusivamente per queste due nuove istruzioni (più dettagli in un prossimo articolo che tratterà anche i costi di implementazione di APX).

Sono decisioni, insomma, che lasciano più che altro l’amaro in bocca per un lavoro che poteva esser fatto sicuramente molto meglio e in maniera decisamente più utile. Ma forse c’è ancora margine per alcuni cambiamenti, considerato che non esiste ancora alcun processore commercializzato con queste estensioni.

Nel prossimo articolo, come già anticipato, ci saranno delle riflessioni sul possibile impatto di APX riguardo alla densità di codice. Infatti è stato già citato talmente tante volte che adesso merita di essere approfondito, essendo un argomento di fondamentale importanza quando si parla di architetture dei processori, e il quale ha fatto e continuerà a fare storia e letteratura in quest’ambito.

Press ESC to close