MegaLab.it
Stampa Articolo
Aperiodico gratuito di informatica
 
20080829214213

CISC vs RISC

a cura di atomik
17/04/2005 - articolo
Hardware & Periferiche - Lo scontro tra due filosofie di progettazione dei microprocessori..

Introduzione

A chi è appasionato di tecnologia, ed è ingordo di informazioni sugli ultimi ritrovati tecnologici, spesso capita di leggere termini come Out of order execution, pipeline, architettura superscalare, branch prediction, sugli articoli che riportano informazioni tecniche sulle CPU moderne.

Per dare la possibilità a chi non conosce bene l'inglese di leggere qualcosa di più approfondito, e per sfatare alcuni miti, ho scritto questo articoletto, in cui parlerò della differenza tra i processori CISC e RISC, e introdurrò alcuni concetti che poi serviranno per una serie di articoli futuri sui processori più interessanti.

SiGenFab2.jpg

L'evoluzione

Chiariamo tanto per cominciare una cosa, l'evoluzione dei processori moderni, è legata a tanti fattori, fra cui compilatori buoni, tecniche di costruzione sempre più raffinate, ma il punto fondamentale è il costo!

Non è importante se un progetto, sulla carta, è decisamente migliore di tutti gli altri, se esso ha costi troppo elevati, e/o il mercato è troppo di nicchia per poter coprire le spese di progettazione e produzione, esso non avrà successo.

I primi passi

Per capire meglio come si sono sviluppate le CPU in questi anni, e il perché alcune hanno avuto successo, mentre altre sono state relegate a mercati di nicchia, dobbiamo partire da lontano:

Negli anni 70, furono introdotti i primi calcolatori basati su transistor integrati, e la situazione era la seguente:

1) le memorie erano lente, grosse e molto costose (erano a nucleo magnetico - vedi foto sotto)

2) i compilatori producevano codice "sporco", e poco ottimizzato, in parole povere erano talmente inefficienti, che era conveniente lavorare in "assembler", il codice scritto però era difficile da correggere (essendo un linguaggio di basso livello).

3) la memoria secondaria, anch'essa magnetica, era ancora più lenta ed era anch'essa costosa. Questo obbligava a scrivere programmi molto compatti e il più possibile ottimizzati, dato che dovevano poi risiedere in una memoria da pochi KBytes.

Memorie.jpg

L'avvento delle memorie a semiconduttore

La situazione migliorò un poco con l'avvento delle memorie dinamiche a semiconduttore a metà degli anni ‘70, ma lo stato di integrazione era di poche migliaia di transistor, e per ottenere un dispositivo completo, era necessario collegare più chip.

Ogni chip ovviamente, assolveva a funzioni differenti, ed erano tutti collegati alla medesima scheda madre.

Il problema di avere molti chip con diverse mansioni, è che le interconnessioni tra di essi (non sono altro che le piste di rame sulla scheda madre, che vengono chiamate anche BUS) hanno una banda limitata, e questo introduceva ulteriori ritardi nel trasferimento dei segnali digitali da una parte all'altra (il problema della banda limitata nasce dal fatto che tra le piste adiacenti c'è un effetto di diafonia, che si accentua quanto più elevata è la frequenza).

Anche oggi alcune componenti sono interessate da problemi simili, in particolare stò parlando del collo di bottiglia costituito dall'interfaccia memoria-chipset e chipset-processore.

Il primo microprocessore General-purpose

DSCF1297.jpg

La soluzione all'epoca fù quella di costruire un singolo chip con tutta la logica necessaria, fù Intel nel 1971 a introdurre il primo processore general-purpose (per applicazioni generali), si chiamava 4004, era composto da 2300 transistor, ed era a tutti gli effetti un "mini-computer".

Esso poteva decodificare al massimo 45 istruzioni, aveva dei registri da 4 bit, e aveva una frequenza di circa 1 Khz (KiloHertz, il prefisso Kilo sta a indicare le migliaia).

Vantaggi di un processore in un unico chip

Certo, oggi fa ridere un chip cosi, ma all'epoca fu una cosa rivoluzionaria, e ci si rese conto che un microprocessore era superiore a un'accozzaglia di chip interconnessi tra di loro da bus, per tutta una serie di motivazioni, che elencherò brevemente:

1) occupa meno spazio

2) ha dei consumi minori

3) produce meno calore

4) è più affidabile di un'insieme di chip saldati su una scheda madre

5) è più veloce, dato che i segnali elettrici devono percorrere cammini più brevi, quindi è possibile operare a frequenze più elevate

6) è più economico da produrre in grosse quantità

Come nasce l'approccio CISC

La situazione che si presentava, a chi progettava allora i chip era questa: le memorie costavano un'occhio della testa, i compilatori erano poco efficienti, quindi perché non progettare chip che eseguissero in hardware anche istruzioni molto complesse?

Per capire meglio, faccio uno stupido esempio abbiamo il seguente programma:

Il programma non fa altro che prendere un valore A=3, memorizzare in B il cubo di questo valore. Ovviamente il programma non serve a niente di utile, ma vediamo come verrebbe lo stesso programma dopo che il compilatore produce in uscita il linguaggio assembler:

L'elevamento al cubo, come vedete, viene eseguita da due istruzioni separate nel linguaggio assembler, mentre noi nel linguaggio di alto livello la eseguiamo in un'unica operazione.

Questo accade perché l'ipotetico processore che dovrebbe eseguire il calcolo, non ha in hardware la funzione eleva al cubo, quindi quando il compilatore riceve come target per la compilazione quel tipo di processore, traduce l'istruzione con due istruzioni separate equivalenti.

In pratica l'ISA (Instruction Set Architecture) di quel processore, tra le operazioni che può eseguire, non contempla la funzione cubo.

Se invece la nostra ISA disponesse di un'istruzione di eleva al cubo, il codice assembler sarebbe stato del tipo:

A differenza di prima, adesso il codice assembler ha lo stesso numero di istruzioni del codice di alto livello, ed è anche più compatto.

Operando in questo modo, è più facile per chi programma in assembler scovare eventuali errori di programmazione (quando viene fatto il debug dell'applicazione).

Un approccio di questo tipo, dove si cerca di avere un'ISA che abbia tutte le istruzioni possibili, e che faciliti il debug, è detto approccio CISC (Complex Instruction Set Computing), in pratica un'architettura composta da un insieme di istruzioni complesse.

È vero che abbiamo aiutato i programmatori a effettuare il debug del software (problema parecchio sentito all'epoca, poiché le memorie erano costose e i programmi dovevano essere quanto il più possibile ottimizzati), ma la fase di decodifica delle istruzioni complesse, è stata demandata al microprocessore.

Ogni nuova CPU che veniva introdotta, doveva essere retrocompatibile con quelle precedenti, altrimenti non si sarebbero potuti far girare i vecchi applicativi.

Le istruzioni CISC

Parliamo ora della memoria, dato che all'epoca non esisteva il concetto di cache (una piccola memoria molto veloce da affiancare alla RAM per velocizzare l'accesso ai dati), e bisognava mantenere la compatibilità con i set di istruzioni precedenti, la maggior parte delle istruzioni prevedevano molti accessi in RAM, sia in lettura che in scrittura.

La maggior parte delle istruzioni CISC, fa uso di modalità di indirizzamento complesso, per ridurre la dimensione del codice compilato.

Per fare un esempio, per eseguire la somma di due celle di memoria:

Questo codice, non fa altro che prendere i due valori da sommare in memoria centrale, il primo all'indirizzo 30 e il secondo all'indirizzo 90, li somma, e poi memorizza il risultato nell'indirizzo di memoria 100.

In totale abbiamo 4 operazioni, che per la filosofia CISC sono troppe.

Per ridurre le dimensioni del codice compilato, basterebbe aggiungere al set di istruzioni, un'istruzione che esegua quelle già viste, prendendo quei 3 parametri come argomento, ad esempio:

# SUM %100,%30,%90

Un bel risparmio di righe di codice, e anche un risparmio in fase di debug.

I problemi dell'approccio CISC

Certo, come penso immaginate, tutta questa semplicità ha un costo, dato che diminuisce notevolmente l'efficienza di esecuzione del codice.

Se riduciamo il numero di istruzioni che compongono il programma, rendendole più complesse, è anche ovvio che il processore per eseguire queste istruzioni complesse, avrà bisogno di più cicli di clock.

I problemi di questo approccio sono i seguenti:

1) le istruzioni hanno molti accessi in memoria centrale, che essendo lenta, rallenta anche l'esecuzione del programma

2) abbiamo molto istruzioni nell'ISA, alcune anche molto complesse; esse vanno decodificate prima di poter essere eseguite.

Quest'ultimo punto, incide su un parametro importante per calcolare le performance di una CPU, le istruzioni eseguite per ciclo di clock.

Per eseguire un'istruzione del tipo SUM %100, %30, %90, il processore doveva eseguire un certo numero di istruzioni, fra cui 3 accessi in memoria, due in lettura e uno in scrittura, e poi il calcolo vero e proprio.

Tutte queste istruzioni devono essere eseguite in maniera sequenziale, e nel giusto ordine. Il processore, può eseguire le istruzioni in due modi, per esecuzione diretta (eseguendo senza doverle decomporre in istruzioni più semplici), e per interpretazione, scomponendo l'istruzione in tante istruzioni più semplici.

Poiché l'ISA che stiamo descrivendo avrebbe centinaia di istruzioni, eseguirle direttamente richiederebbe per l'implementazione in hardware un numero improponibile di transistor! (non posso spiegare tecnicamente come si implementano i circuiti che poi eseguono i calcoli veri e propri, che probabilmente farebbe capire meglio la cosa, ma mi riprometto prima o poi di fare un'articolo al riguardo, ma forse sarebbe solo per i duri di stomaco : -)).

La soluzione a questo problema, è quella di avere all'interno del processore una ROM (memoria in sola lettura) che contiene la traduzione delle istruzioni complesse in sequenze di istruzioni semplici.

Praticamente, non siamo più noi (o il compilatore) a scomporre le istruzioni complesse in tante istruzioni semplici, ma è il processore che si preoccupa di scomporle in istruzioni direttamente eseguibili in hardware.

Questa operazione di decodifica, è un punto fondamentale dei processori CISC, dato che, più sono complesse le istruzioni, più tempo ci vorrà per decodificarle.

Non so se vi siete accorti, che a poco a poco vi stò elencando tutta una serie di svantaggi dell'approccio CISC, che poi portarono alla nascita della filosofia RISC.

Il Critical Path

Image11.jpg

Vi siete mai chiesti perché alcuni processori possano raggiungere i 700 MHz, mentre altri raggiungono i 3 GHz?

Certo, molto spesso cercando in giro troverete risposte del tipo: costruendo i processori con tecnologie a 130 nanometri, si riduce la dimensione dei transistor, e quindi si può salire in frequenza.

Questo è vero, ma a mio avviso non è una risposta completa, dato che ci sono motivi più specifici.

Ovviamente potete intuire dal titolo che la risposta a questa domanda è proprio il critical path (cammino critico), ossia il percorso più lungo che un segnale deve attraversare in un singolo ciclo di clock.

Se in un qualche punto esiste un percorso che un segnale impiega 1 microsecondo a percorrere, il clock massimo per quel processore sarà di 1 MHz (ossia l'inverso di 1 microsecondo).

Non importa poi se in tutti gli altri percorsi, i segnali impieghino un decimo di questo tempo, basta un solo percorso dove il segnale è lento, da costringere tutto il resto ad andare alla "sua" velocita.

Oltre questo, per problemi legati alla deriva termica (al variare della temperatura, variano alcune caratteristiche elettriche), all'invecchiamento dei componenti, e al fatto che i transistor non sono mai precisi come sono stati progettati sulla carta (per inevitabili difetti di costruzione), occorre tenersi ben al di sotto di quel margine di 1 MHz di cui abbiamo parlato prima.

Tab_003.jpg

Come nascono i processori RISC?

Cosa c'entra tutto questo con questo articolo? Semplice, per ovviare al problema del critical path, si ricorre alle pipeline, che nascono con l'approccio RISC.

Il fatto che un processore CISC possa eseguire una sola istruzione per volta, fa si che i circuiti adibiti allo svolgimento dell'esecuzione più complessa, siano quelli più lenti, e che quindi questi finiscano con il rallentare tutto il processore.

Considerazioni che portano alla nascita dei RISC

Con il passare degli anni, quando i compilatori erano divenuti molto più efficienti, e le memorie erano molto meno costose, i progettisti cominciarono a fare le seguenti considerazioni:

1) Dato che, test effettuati, risultò che per il 90% del tempo, il processore utilizza sempre un piccolo sottoinsieme di istruzioni, perché non ottimizzare il processore nell'esecuzione di queste poche istruzioni, lasciando al compilatore il lavoro di decodifica delle istruzioni più complesse in istruzioni più semplici?

2) Se il processore è ottimizzato per eseguire direttamente queste poche istruzioni, perché non fare in modo che ogni istruzione venga completata in un singolo ciclo di clock?

3) Dato che le istruzioni con indirizzamento complesso rallentano molto l'esecuzione dei programmi (richiedono parecchi accessi in memoria centrale), è meglio eliminare queste istruzioni, e lasciare solo due comandi di load e store, rispettivamente per caricare, e memorizzare i dati dai registri del processore alla memoria centrale.

4) Considerando che dobbiamo limitare gli accessi in memoria centrale, è necessario avere un numero sufficiente di registri per consentire l'elaborazione dei dati.

Un esempio per capire meglio

Facciamo un piccolo esempio di codice per capire meglio le ultime due considerazioni:

Questo breve codice, non fa altro che inizializzare i valori di i e j a 0 al primo ciclo, poi in ogni ciclo, il valore corrente di i viene aggiunto a j, e il risultato viene memorizzato in j, finito questo i viene incrementato di 1 (tramite l'istruzione i++, che sarebbe come dire i = i+1), e quando i raggiunge il valore 100 si esce dal ciclo.

Se il compilatore fa un buon lavoro, per salvare i valori di i e j utilizzerà due registri locali del processore, e solo alla fine del ciclo memorizzerà i rispettivi valori in memoria centrale.

Questo è però vero per un processore RISC, un processore CISC invece, potrebbe benissimo effettuare tutti gli accessi in memoria centrale per aggiornare i valori di i e j.

Sfruttando la semplicità delle istruzioni di un processore RISC, è possibile fare in modo che esse vengano eseguite in cascata su più istruzioni, come in una catena di montaggio.

Art17e.gif.gif

Il risultato è che è possibile eseguire le istruzioni in un singolo ciclo di clock, caratteristica che è stata a lungo una peculiarità dei processori RISC.

Ma come è possibile tutto questo, se sappiamo benissimo che un Athlon ha una pipeline da 10 stadi, e un Pentium 4 ne ha addirittura 20? Semplice, essi non sono processori CISC, ma sono processori ibridi CISC-RISC.

Processori Ibridi CISC-RISC

L'architettura interna dei processori odierni, ha poco o nulla a che fare con l'approccio CISC che abbiamo visto prima.

Essi sono ancorati al passato, solo perché devono essere retrocompatibili con i vecchi programmi.

I nuovi processori (K7 o P4 che siano), decodificano si il codice x86, ma una volta decodificato, esso viene mandato in esecuzione su una serie di stadi di elaborazione di tipo RISC.

26749a-08_duo.jpg

Facciamo quindi queste considerazioni:

1) Il codice x86, che è complesso (tante istruzioni di natura diversa), e disomogeneo (le istruzioni possono essere lunghe 8, 16 e 32 bit), viene decodificato attraverso ROM che contiene la corrispondenza tra le istruzioni complesse e quelle più semplici, e che asserve alla decodifica delle stesse.

2) Alle istruzioni dell'ISA x86, vengono sostituite le istruzioni in formato omogeneo (a 32 bit) che possono essere quindi immesse in una memoria tampone (buffer) in attesa di essere processate dalle unità di elaborazione.

Le istruzioni decodificate sono RISC-like, nel senso che appartengono ad un "alfabeto" meno ricco del codice nativo ISA x86 ma altamente ottimizzate.

È evidente che l'architettura RISC ha avuto la meglio, e che i progettisti Intel e AMD hanno fatto un ottimo lavoro, che ha consentito ai processori x86 di sopravvivere fino a oggi, oltre ogni aspettativa

(Ma credo che se Intel ha deciso di cominciare con L'IA-64, per l'ISA x86 è giunto il momento del pensionamento).

Appuntamento ai prossimi articoli..

Si conclude qui questo primo articolo, ne prossimi articoli introdurrò i processori PowerPC e G4 (per intenderci, quelli che erano montati nelle macchine Apple Macintosh, e quelli che sono montati ancora oggi nelle macchine entry level e nei portatili), poi seguiranno a ruota articoli di approfondimento su Pentium 4, Athlon, Processori PPC 970 (quelli montati sugli ultimi Apple Macintosh G5), Power4 e Power 5 di IBM (quelli da cui è stato derivato il PPC 970), e se resterà tempo approfondirò anche i processori SPARC (quelli montati sulle macchine di Sun Microsystems).

PPC970-2.jpg

MegaLab.it rispetta la tua privacy. Per esercitare i tuoi diritti scrivi a: privacy@megalab.it .

Copyright 2008 MegaLab.it - Tutti i diritti sono riservati