Il definitivo RISC vs CISC – 4: Come i RISC divennero CISC

La macchina del fango contro i CISC della propaganda messa in piedi dagli evangelisti dei RISC, di cui abbiamo già discusso nel precedente articolo, è stata molto attenta a puntare continuamente e incessantemente il dito su una sola direzione, ma ha silenziosamente evitato di affrontare le problematiche che si sono poi rivelate fatali per i loro beneamati.

Non è che i RISC siano del tutto scomparsi dal panorama delle architetture degli elaboratori, ma possiamo certamente affermare, definizione e storia alla mano, che la quasi totalità dei processori esistenti non siano RISC, ma debbano essere necessariamente classificati come CISC.

Le motivazioni sono già state espresse nei precedenti articoli (basta, banalmente, applicare pedissequamente la definizione di RISC), ma penso sia di particolare importanza capire come questa trasformazione sia avvenuta.

Ovvero in che modo, a seguito dei progressi della tecnologia, i RISC siano, di fatto, diventati dei CISC (a dispetto dei proclami che, purtroppo, continuano tuttora) nel momento in cui sono miseramente crollati la maggior parte dei pilatri su cui erano stati fondati (ma è sufficiente che ne fosse crollato anche soltanto uno).

Cade il quarto pilastro: “Le istruzioni devono essere semplici –> eseguite in un singolo ciclo di clock”

Il rapido aumento delle frequenze di clock dei processori è stato il primo dei progressi tecnologici che ha attanagliato i fautori dei RISC. Infatti l’adozione di processi produttivi sempre più avanzati ed efficienti (ben illustrati dalla famosa Legge di Moore) ha fatto iniziare una corsa al raggiungimento di frequenze sempre più elevate per i processori, a cui non è seguito un corrispondente aumento delle frequenze per le memorie, le quali hanno ottenuto avanzamenti di gran lunga inferiore rispetto i primi.

Ciò ha comportato l’introduzione di un disaccoppiamento nell’interfacciamento dei processori verso la memoria: per queste ultime, infatti, sono stati utilizzati bus di collegamento a frequenze più ridotte rispetto a quelle dei processori, inizialmente introducendo il concetto di moltiplicatore (rispetto alla frequenza del bus) per ottenere la frequenza del processore (come i famosi 486DX2 di Intel), per poi rendere totalmente indipendenti le due frequenze.

Questo disallineamento fra le due frequenze di funzionamento ha comportato, quindi, che l’accesso alla memoria, da parte dei processori (o altri dispositivi), ha richiesto più cicli di clock per il completamento dell’operazione (cosa che, comunque, avveniva già da un po’ di tempo coi famigerati wait state).

Tutto ciò ha avuto ricadute pratiche sull’esecuzione delle istruzioni, laddove queste dipendessero direttamente dall’accesso alla memoria (le famose load / store) o indirettamente (per caricare le istruzioni da eseguire; non soltanto a causa di un salto a un diverso indirizzo).

A complicare ulteriormente le cose ha contributo anche l’introduzione della virtualizzazione della memoria (e non solo. Ma evito di complicare il discorso, perché non è strettamente funzionale allo scopo dell’articolo), che ha introdotto dei “livelli” per l’accesso alla memoria e che, quindi, può comportare l’aggiunta di altri cicli di clock per completare l’operazione.

Dulcis in fundo, anche l’implementazione di una pipeline per le istruzioni, la cui esecuzione è stata suddivisa in diversi “passi” / “stadii” (stage) più semplici, così da far funzionare il processore a frequenze più elevate (la frequenza raggiungibile è dominata dalla velocità di esecuzione del componente circuitale più lento), ha aggiunto altri cicli di clock.

Il che ha funzionamento sicuramente molto bene quando le istruzioni non dovessero accedere alla memoria (perché ci sarebbe stato un stallo nella pipeline, finché il dato non fosse finalmente arrivato) oppure non dovessero cambiare l’indirizzo di esecuzione (saltando a un altro pezzo di programma), perché questo ha, ovviamente, comportato l’introduzione di cicli di clock addizionali finché non fosse possibile riprendere nuovamente l’esecuzione dell’istruzione.

Sono stati introdotti meccanismi (cache L1/L2/L3, cache TLB, cache BT fra i più famosi) per cercare di risolvere tutti questi problemi, ma ovviamente non sono in grado di garantire il completamento delle istruzioni in un ciclo di clock, nemmeno presi tutti assieme.

Considerato che i processori moderni sono in grado di eseguire, sulla carta, più istruzioni per ciclo di clock (il famigerato IPC: Instructions Per Cycle), per un processore eseguirle tutte in ciclo di clock vorrebbe dire viaggiare alla massima velocità, quindi con IPC coincidente col numero massimo possibile. Ma è chiaro che, per quanto già detto, ciò è sostanzialmente impossibile.

Conseguenza di tutto ciò è che il quarto pilastro su cui si fondavano i RISC è, di fatto, crollato.

Cade il primo pilastro: “Dev’esserci un insieme ridotto di istruzioni”

La già citata Legge di Moore ha solleticato, nel tempo, l’appetito dei produttori di processori, sempre alla ricerca di migliorarne le prestazioni. Cosa che si è concretizzata aggiungendovi nuove, più potenti, istruzioni grazie alla disponibilità di una quantità esponenzialmente sempre più elevata di transistor da utilizzare (ricordo che, secondo tale legge, la loro quantità impacchettata nei chip raddoppierebbe ogni due anni).

In questo modo, ad esempio, ai RISC sono state inserite istruzioni di moltiplicazione o, addirittura, di divisione (!), in barba al fatto che richiedano più cicli di clock (parecchi, per le divisioni!) per completare la loro esecuzione. Ma il quarto pilastro era già caduto, per cui tentare disperatamente di preservarlo a tutti i costi non avrebbe più avuto alcun senso.

Tante altre istruzioni sono entrate, quindi, a far parte dell’ISA dei processori RISC (ovviamente anche dei CISC, ma… questo lo hanno fatto da quando sono nati!): quelle per i calcoli in virgola nobile (le quali sono ben note per le loro latenze ben più lunghe di quelle “intere”), SIMD (alcune con istruzioni particolarmente complicate), lock / primitive di sincronizzazione, crittografia, firma digitale, bit, campi di bit, ecc. ecc. ecc., con una pletora a richiedere più e più cicli di clock per la loro completa esecuzione (e, quindi, continuando a infierire sul succitato quarto pilastro).

Inutile rimarcare che, aggiungendone sempre nuove, l’insieme di istruzioni è andato aumentando, smantellando il primo pilastro dei RISC: il loro insieme non era più ridotto

Va sottolineato che i CISC sono sempre stati oggetto di scherno (ancora oggi!) perché integravano diverse istruzioni, come quelle per gestire tipi di dati BCD, manipolare stack frame complicati (per supportare meglio linguaggi di programmazione che consentivano la definizione e l’uso di funzioni annidate; come, ad esempio, Pascal e derivati), stringhe / blocchi di memoria, ecc., per esattamente la medesima ragione: migliorare le prestazioni nei casi comuni (dell’epoca)!

I RISC hanno fatto, e continuano a fare, proprio lo stesso e oggi abbiamo processori che integrano anche centinaia di istruzioni. Ma nessuno, però, li ha biasimati per questo! Anzi, ed esattamente al contrario, tali estensioni sono viste favorevolmente e vendute & pubblicizzate come grandi innovazioni.

Coerenza? Non pervenuta!

Magari in futuro gli attuali processori / architetture saranno nuovamente messi alla berlina poiché certe funzionalità saranno scaricate su coprocessori o chip esterni (com’è possibile vedere con l’esplosione dell’IA, che si nutre pesantemente di appositi chip/unità altamente specializzati), per cui le istruzioni per tali compiti verranno viste come un intollerabile peso ed eredità. Il cerchio della vita, che si ripete…

Cade il terzo pilastro: “Le istruzioni devono avere una lunghezza fissa –> no alla loro lunghezza variabile”

Infine, ma non meno importante, i produttori di RISC sono stati attanagliati da un micidiale tarlo che, da sempre, si porta dietro questa famiglia di processori: la scarsa densità di codice (per la quale sono ben noti. Certamente non in termini positivi!).

Questo perché ciò ha diverse implicazioni: maggior spazio richiesto (la memoria ha i suoi costi), maggior banda di memoria consumata (su tutta la gerarchia: dalle cache alla memoria di sistema), cache di maggiori dimensioni (anche qui parliamo di costi) e, infine, di maggiori consumi (bisogna pur sfamare tutti questi transistor addizionali).

Aggiungere nuove istruzioni per fare più “lavoro utile” ha aiutato in questo senso (meno istruzioni per svolgere gli stessi compiti significa che viene richiesta meno memoria per il codice), ma non è stato abbastanza per risolvere questo problema generale.

Per cui tali produttori hanno deciso, infine, di “prendere in prestito” l’ultima, eretica (!), caratteristica tipica di molti CISC: le istruzioni a lunghezza variabile! E anche il terzo pilastro viene disintegrato…

Una cosa importante da sottolineare su quest’argomento è che tanta gente asserisce che, avendo parecchia disponibilità di memoria di questi tempi (e anche parecchia banda di memoria), la densità di codice non sia più importante e, quindi, non sarebbe necessario salvare qualcosa in quest’ambito.

I produttori e gli architetti di processori, però, dissentono sonoramente da (esilaranti; a dir poco) prese di posizione come queste, e continuano a definire nuove architetture con istruzioni a lunghezza variabile oppure a riservare una parte (anche cospicua!) delle loro ISA specificamente allo scopo.

Ad esempio una delle architetture più nuove e moderne, RISC-V, ha riservato ben il 75% del suo intero spazio degli opcode per le cosiddette istruzioni “compatte” (aventi 16 bit di ampiezza anziché i canonici 32), a cui si aggiunge un’altra parte (del rimanente 25%) per istruzioni ancora più lunghe (fino a 22 byte. Più una parte riservata per istruzioni… ancora più lunghe!). A confronto, x86/x64, che arrivano a 15 byte al massimo per istruzioni, sembrano dei dilettanti…

ARM rappresenta una singolare eccezione, poiché non ha (ancora) definito nessuna “estensione compatta” alla sua ISA a 64 bit (ARMv8. E adesso anche ARMv9). Il che suona davvero strano, considerato l’enorme impegno profuso nelle sue Thumb e Thumb-2 (per l’architettura a 32 bit), che hanno pure loro fortemente contributo al suo successo (tanto che esistono parecchi processori ARM con la sola Thumb-2 implementata).

Esiste una spropositata quantità di studi sull’importanza della densità di codice, ma avendo affrontato già l’argomento nella serie sulla nuova architettura APX di Intel preferisco riportarne un estratto di una delle più recenti e interessanti pubblicazioni in merito, dalla tesi di uno dei progettisti di RISC-V:

Waterman shows that RVC fetches 25%-30% fewer instruction bits, which reduces instruction
cache misses by 20%-25%, or roughly the same performance impact as doubling the instruction
cache size
.

Come risulta evidente, la densità di codice ha serissime implicazioni a livello microarchitetturale, ed è la ragione per cui, oltre alla caterva di pubblicazioni, le nuove architetture vi aggiungono esplicitamente supporto, oppure vengano direttamente sviluppate attorno a essa (come ARM con le ISA Thumb, ma anche la stessa RISC-V).

Il che passa necessariamente dall’adozione di un insieme di istruzioni a lunghezza variabile, in quanto è la soluzione architetturale che offre i migliori benefici in quest’ambito (anche se a farne le spese saranno i frontend dei processori, che diventeranno più complicati. Qui dipende molto pure dal come siano stati progettati gli opcode delle istruzioni).

Conclusioni

Direi che non ci sia molto altro da aggiungere, a parte che tutto ciò mi riporta alla mentre sempre La fattoria degli animali: <<Quattro gambe buono, due gambe meglio>>.

Perché è veramente impressionante come questo romanzo descriva alla perfezione la trasformazione dei RISC in CISC, e come questa sia avvenuta. E’ stato a dir poco profetico…

Il prossimo articolo verterà, invece, sui CISC e di come… siano rimasti gli stessi!

Press ESC to close