Le Ragioni per Computer con Insieme di Istruzioni Complesse

Ne avevo già accennato nella serie di articoli riguardanti l’atavica diatriba sui RISC vs CISC, e adesso è arrivato il momento di affrontare la questione di petto riproponendo, ma questa volta a parti invertite, le ragioni per cui oggi più che mai abbia senso rivolgere l’attenzione verso architetture CISC anziché su quelle RISC (che in realtà non sono più tali, come già dimostrato per l’appunto).

I quattro pilastri su cui sono stati basati i RISC sono esattamente le ragioni per le quali ha perso di significato cercare di imporre questi limiti, ormai chiaramente artificiosi se non prettamente ideologici / dogmatici, ad architetture e microarchitetture.

Potevano andare bene quando il concetto di RISC si è sviluppato e coi primi processori di questa macrofamiglia, ma l’evoluzione tecnologica e le mutate esigenze dei sistemi informatici hanno smantellato una a una queste fondamenta, come possiamo appurare dalla seguente analisi.

1. Dev’esserci un insieme ridotto di istruzioni

Mantenere poche istruzioni in un’architettura poteva andare bene quando i chip avevano dimensioni ridotte in termini di transistor, poiché i processi produttivi a disposizione consentivano di impacchettarne una quantità limitata in un determinato spazio, ma la famosa Legge di Moore ha dimostrato che il problema sarebbe stato risolto velocemente, nel giro di pochi anni.

A un certo punto, quindi, l’abbondanza di transistor ha portato, semmai, a porsi il problema di come impiegarli. Certamente aggiungere cache per codice e/o dati è un buon modo per farlo, come pure aggiungere unità di predizione dei salti sempre più complesse, o ancora rimpolpare le cache TLB, fino ad aggiungere più pipeline per l’esecuzione delle istruzioni.

Tutto ciò contribuisce a migliorare le prestazioni di un processore, ma ci sono casi in cui ciò non basta. Esistono, infatti, compiti specifici che richiedono l’esecuzione di numerose istruzioni per ottenere il risultato desiderato.

Se l’algoritmo è abbastanza piccolo, semplice, e ha pochi input (uno, due. Anche tre, in alcuni casi) si può pensare di aggiungere apposite istruzioni per eseguirlo direttamente in hardware e ottenere significativi aumenti nella velocità d’esecuzione dei programmi, oltre a ridurre lo spazio occupato dal codice (un’istruzione ne rimpiazza diverse).

Sostanzialmente è proprio quello che hanno fatti i primi processori, implementando in hardware operazioni come i calcoli in formato BCD, la manipolazione di stringhe / blocchi di memoria, la creazione e il mantenimento di frame nello stack, ecc. ecc..

Oggi qualunque processore mette a disposizione centinaia o anche migliaia di istruzioni proprio per gli stessi motivi. Cambiamenti ce ne sono stati, è vero, ma riguardano soltanto la tipologia di istruzioni implementate in hardware, poiché ci sono altre esigenze da soddisfare. Quella di avere istruzioni specializzate, però, non è mai venuta meno ed è sempre stata ampiamente soddisfatta.

Si è assistito, dunque, a un ritorno di una delle caratteristiche tipiche di molti (ma non di tutti) CISC: processori con un cospicuo numero di istruzioni in dotazione.

3. Le istruzioni devono avere una lunghezza fissa

Penso sia assolutamente innegabile che avere istruzioni a lunghezza fissa ne semplifichi non poco la loro decodifica, come pure la gestione interna. Ma il rovescio della medaglia è rappresentato dal fatto che la dimensione dei programmi aumenta considerevolmente e/o che aumenti il numero di istruzioni da eseguire, a seconda della dimensione degli opcode.

Fatta eccezione per processori particolari, come i VLIW, i processori RISC in genere hanno avuto istruzioni fisse di lunghezza pari a 16 o 32 bit. Entrambe le dimensioni presentano dei pregi e dei difetti, ovviamente.

Avere opcode di 16-bit consente di migliorare anche moltissimo la densità di codice e, quindi, ai programmi di occupare meno spazio in memoria, con annessi benefici in terminali prestazionali e di consumi per tutta la gerarchia di memoria.

Per contro, possono utilizzare pochi registri e/o mettere a disposizione poche istruzioni e/o richiedere l’esecuzione di più istruzioni per assolvere a un determinato compito. Il che incide, quindi, negativamente sulle prestazioni (e anche sui consumi, se devono essere eseguite più istruzioni per lo stesso compito).

In particolare va sottolineato che limitarsi a opcode di soli 16 bit diventa un forte handicap per un processore moderno e più general-purpose, poiché oggi vengono integrate centinaia di istruzioni che coprono parecchi utilizzi (FPU, SIMD/vettoriali, criptazione/hashing, ecc. ecc.).

Viceversa, opcode di 32 bit non soffrono di tutti questi problemi, ma sono particolarmente detrimenti per quanto riguarda la densità di codice, pur potendo contare sulla possibilità di utilizzare valori immediati oppure offset (per i salti o l’indirizzamento alla memoria) di ampiezza ben maggiore rispetto a quanto possibile con opcode a 16 bit.

Per questi motivi molti processori che vengono spacciati come RISC (abbiamo visto nella serie dedicata che, in realtà, non lo sono affatto!), ossia con architetture L/S (Load/Store), hanno implementato un’ISA che usano opcode a 16 e 32 bit, in modo da cercare di ottenere i vantaggi di entrambi, limitando al contempo la complicazione di gestire opcode di lunghezza diversa (ce ne sono soltanto due di cui tener conto). Diventano, quindi, architetture a lunghezza variabile.

D’altra parte non ci sono molti margini: il passaggio a ISA a lunghezza variabile è sostanzialmente l’unica strada per ottenere una buona densità di codice e, al contempo, poter contare su un’architettura più generale. ISA L/S con opcode di soli 16 bit con ottima densità di codice esistono, ma sono poco generali, infatti (vanno bene in ambito embedded, ad esempio).

Hanno avuto ben pochi problemi, invece, i CISC, che spesso hanno potuto contare su opcode a lunghezza variabile e non limitati esclusivamente a 16 o 32 bit, grazie ai quali è stato possibile specificare valori immediati o offset di generose dimensioni, o anche istruzioni aventi più funzionalità (basti vedere i limiti di RISC-V per le istruzioni vettoriali, ad esempio, che consentono di poter utilizzare un solo registro per la maschera / predicazione, causa mancanza di spazio negli opcode a 32 bit).

E’ bene sottolineare l’importanza di tutto ciò, perché la possibilità di poter specificare grandi valori immediati o offset in una sola istruzione comporta, in ISA che non lo consentono, l’uso di registri addizionali e l’esecuzione di più istruzioni, fra loro dipendenti (quindi che possono introdurre stalli nella pipeline), con evidente ricadute (negative) in ambito prestazionale e densità di codice.

Il vantaggio dei CISC risulta piuttosto evidente.

4. Le istruzioni devono essere semplici

Eseguire istruzioni in un solo ciclo di clock è uno dei cardini dei RISC, che per questo hanno limitato l’ISA a istruzioni piuttosto semplici pur di garantire il rispetto di questo requisito, ovviamente ad assoluto beneficio delle prestazioni.

In realtà un altro dei motivi per cui è stato imposto l’utilizzo di istruzioni semplici è che in tal modo si sarebbero potute raggiungere frequenze d’esercizio più elevate per i processori, impattando ancora una volta (positivamente) sulle prestazioni.

Il sogno di raggiungere frequenze elevate si è, però, schiantato contro i limiti dei processi produttivi e, in generale, dei materiali utilizzati, in quanto lavorare a frequenze più elevate ha fatto esplodere i consumi dei chip alzando, di fatto, un muro sulle prestazioni relativamente a questo parametro.

Dall’altro lato, limitarsi a istruzioni semplici ha comportato l’esclusione di tantissime nonché molto utili operazioni, come quelle celeberrime dedicate ai calcoli con valori in virgola mobile, tanto per fare l’esempio più eloquente.

Si tratta, inutile dirlo, di una grossissima limitazione che avrebbe tarpato le ali a qualunque processore nel giro di poco tempo e che, infatti, è stata molto velocemente messa da parte e ignorata. Non per i CISC, ovviamente, che hanno sempre avuto a che fare con istruzioni complicate e che hanno richiesto anche più cicli di clock per il completamento della loro esecuzione.

Bisogna anche puntualizzare che questo dogma di fede influenza anche i precedenti due. Infatti essere costretti a implementare soltanto istruzioni semplici spesso vuol dire fare a meno di quelle a lunghezza variabile, che in genere sono più complicate.

Significa pure limitare molto l’insieme delle istruzioni implementabili nonché la loro utilità, ponendo di conseguenza dei freni alle prestazioni.

2. Soltanto le istruzioni di load/store possono accedere alla memoria

Fatti fuori i precedenti pilastri, rimane ormai in piedi soltanto l’ultimo totem a cui rimangono saldamente aggrappati gli ex-RISC (ormai L/S): l’accesso alla memoria esclusivamente tramite istruzioni di load / store. La quale è sostanzialmente l’unica caratteristica che li differenzia in maniera netta dai CISC (anche se, in realtà, questi potrebbero tranquillamente essere L/S).

Le motivazioni sono abbastanza evidenti: relegare gli accessi in memoria soltanto a queste istruzioni semplifica la loro decodifica, perché soltanto poche istruzioni devono essere “intercettate” per poi essere smistate all’apposita pipeline.

Mentre architetture non L/S possono avere parecchie istruzioni potenzialmente in grado di accedere alla memoria, complicandone quindi la decodifica (bisogna intercettarle tutte). Inoltre alcune microarchitetture possono decidere di generare più micro-op per un’istruzione, complicando ancora il frontend del processore (che deve instradare più micro-op al backend, tenendo conto della loro dipendenza).

Considerazioni di cui tenere conto, senz’altro, ma che magari sono legate un po’ troppo ai tempi in cui c’erano pochi transistor a disposizione per implementare tutto ciò, e che non valgono più o che sono estremamente ridimensionate tenuto conto dell’enorme numero di transistor a portata di mano, oltre che dalla notevole complessità che i processori moderni hanno raggiunto a prescindere da tutto.

Ciò che viene artatamente nascosto o minimizzato è il fatto che avere istruzioni generiche in grado di accedere direttamente alla memoria comporti anche dei notevoli vantaggi.

Infatti essere costretti a usare soltanto istruzioni di load per caricare valori da utilizzare comporta quattro cose non di poco conto:

  • l’aggiunta di istruzioni da eseguire;
  • il conseguente peggioramento della densità di codice (più istruzioni occupano più spazio in memoria);
  • l’utilizzo di un registro in cui caricare il valore prima di poterlo utilizzare;
  • lo stallo (di diversi cicli di clock) nella pipeline causato dall’attesa della seconda istruzione per poter leggere il valore dal registro in cui sarà caricato (tecnicamente si chiama penalità da load-to-use).

Considerazioni del tutto analoghe valgono nel caso opposto, cioè quando un’istruzione di store dev’essere utilizzata per copiare in memoria il risultato di un’operazione (con annessa dipendenza dalla disponibilità del risultato).

Tutte cose che non valgono nel caso di processori CISC (non L/S), i quali possono eseguire direttamente una singola istruzione in grado sia di accedere alla memoria sia di elaborarne il dato, risparmiando al contempo un registro (il che significa che ne servono meno in un’ISA del genere) e dovendo pagare il pegno di un solo ciclo di clock, in genere, per il load-to-use (che in sostanza equivale al semplice passaggio allo stadio successivo nella pipeline).

Conclusioni

Penso sia abbastanza chiaro, ormai, come lo scardinamento di tutti e quattro i pilastri su cui erano fondati i RISC comporti vantaggi di gran lunga maggiori rispetto alla maggior complessità richiesta per la loro implementazione.

Persino l’ultimo totem che è rimasto in piedi per gli ex-RISC, cioè l’uso di sole istruzioni load/store, presenta molti più svantaggi rispetto agli evidenti benefici ottenibili diversamente.

E’ a causa di tutte queste considerazioni che, a mio avviso, è necessario ripensare seriamente all’introduzione di architetture CISC, le quali possono riuscire a ottenere prestazioni in singolo core/thread più elevate rispetto alle ISA L/S, in quanto intrinsecamente in grado di poter eseguire più “lavoro utile” per singola istruzione.

Cosa che si rende particolarmente evidente con processori aventi pipeline più semplici (in-order), dove la possibilità di indirizzare direttamente la memoria per le istruzioni consente di risparmiarne di preziose, riducendo anche i conseguenti stalli.

Si deve smettere, quindi, di continuare a demonizzare questa macrofamiglia di processori e ridare nuovamente lustro ai CISC e il giusto posto che si meritano nel panorama delle architetture dei processori.

E’ anche per questo motivo che in una futura serie di articoli presenterò l’architettura CISC a cui ho lavorato negli ultimi anni, la quale fa tesoro di tutti i cardini già illustrati in un panorama abbastanza ristagnante qual è quello delle ISA moderne.

Press ESC to close