| La
nascita dell'approccio CISC |
La situazione che si presentava ai progettisti degli
anni '70 era allora chiara: le memorie erano molto costose,
i compilatori inefficenti e dunque perchè non cercare di rendere
i computer più facilmente programmabili sfruttando le emergenti
tecnologie ad alta integrazione per fare chip sempre più "intelligenti"
in grado di eseguire in hardware istruzioni anche molto complesse?
Per capire meglio il senso di questa frase ricorriamo ad un
semplice esempio:
Programma
in linguaggio generico di alto livello: prende un valore fornito
dall'utente, lo moltiplica per due e ne effettua il cubo.
A = 2;
B = 2 * A;
C =cube(B);
A cosa possa servire un tale programma
non ci interessa; è qui messo solo a scopo didattico. Quanto
appena visto è il listato che un programmatore può produrre.
Il compilatore riceve la stringa di ingresso dei comandi impartiti
e produce in uscita lo stesso codice tradotto in linguaggio
assembler, come evidenziato in basso :
Lo
stesso programma in linguaggio assembler:
MOV A,2; memorizza
nel registro A del processore il valore 2
MUL B,A,2; moltiplica
il valore immagazzinato in A per due e salva il risultato nel
registro B
MUL TEMP,B,B; moltiplica
B per se stesso e salva il risultato in un registro temporaneo
MUL C,TEMP,B; moltiplica
TEMP per B e salva il risultato in C
Notiamo subito che l'operazione "elevamento
al cubo" presente con un solo comando nel linguaggio ad alto
livello è stato "splittata" in due istruzioni separate del linguaggio
assembler ; questo perchè il processore di riferimento, che
è il target del codice prodotto dall'assembler, non dispone
in hardware della funzione "cubo" ma deve ricorrere a due moltiplicazioni
separate, e ad un registro temporaneo di appoggio. In gergo
si dice che l'ISA ( instruction set architecture )
del processore, cioè l'insieme delle operazioni che quel processore
può eseguire, non contempla la funzione "cubo" . Di
conseguenza, è necessario decomporre la funzione cubo in operazioni
più semplici che il processore può esguire. Fin qui tutto semplice,
ma sarebbe bello se il nostro processore disponesse di un' ISA
avanzato che contemplasse anche l'esecuzione di un elevamento
al cubo. Se tale funzione esistesse, l'assembler sarebbe modificato
in questo modo:
Lo
stesso programma in linguaggio assembler con un ISA più potente:
MOV A,2; memorizza
nel registro A del processore il valore 2
MUL B,A,2;
moltiplica il valore immagazzinato in A per due e salva il risultato
nel registro B
CUB C,B;
esegue il cubo di B e salva il risulato in C
Quelle che emergono in questo caso sono
due conseguenze importanti:
1) il codice assembler è in corrispondenza 1-1 col codice
ad alto livello; pertanto è più compatto. 2) se c'è un
errore nel programma, è più facile scovarlo e porvi rimedio
(fase di debugging più semplice). Il primo punto è noto come
chiusura del gap semantico fra il codice ad alto livello
e il codice assembler. Significa semplicemente che per ogni
istruzione ad alto livello non ne occorrono diverse in assembler
più semplici, ma ne basta una.
CISC = Complex Instruction Set Computing
Una filosofia di questo tipo, in cui si cerca di avere
un ISA che sia il più flessibile possibile e che faciliti il
debugging del codice prodotto dai compilatori, è nota come approccio
CISC al progetto di un microprocessore. CISC è un acronimo
e sta appunto per Complex Instruction Set Computing,
cioè architettura costituita da un insieme, o set, di istruzioni
complesse. Fin qui sembrerebbe tutto rosa e fiori: abbiamo solo
aiutato i programmatori a scrivere codice assembler più semplice,
quindi più veloce da correggere, e abbiamo dunque ovviato al
grave problema del costo del debugging del software, che all'epoca
era molto sentito giacchè la memoria era assai costosa e il
codice compilato doveva essere il più possibile denso e ottimizzato.
Già, ma il problema è che abbiamo fatto in modo da demandare
tutto la responsabilità della decodifica di istruzioni complesse
al microprocessore! Ricordate che vi ho detto prima sull'Intel
4004? Che il suo ISA era costituito da 45 istruzioni soltanto.
Negli anni a venire si inserirono nell'ISA dei processori general
purpose come quelli Intel e relativi cloni (AMD per intenderci)
centinaia di nuove istruzioni. Inoltre, ogni set di nuove istruzioni
doveva essere retrocompatibile con quello precedente, pena l'impossibilità
da far girare i vecchi applicativi sui nuovi processori. Il
set ISA della famiglia Intel è noto come set 80x86 (o semplicemente
x86), perchè compatibile con i set di istruzioni del 8086, 80286,
80386 etc, fino ad arrivare all'attuale Pentium4. In breve,
il Pentium4 è progettato per far girare anche applicativi scritti
25 anni fa!
Ma torniamo a noi e ai prodromi dell'architettura CISC.
All'epoca non esisteva il concetto di una memoria veloce ma
piccola (l'odierna cache) da affiancare al processore, e a causa
del mantenimento della compatibilità con i vecchi ISA, la maggior
parte delle istruzioni prevedevano un sacco di accessi in lettura
e scrittura alla memoria di sistema, la RAM, per intenderci
. Vediamo di chiarire le idee con un esempio: possiamo schematizzare
la memoria di sistema come un array, cioè un insieme ordinato,
di celle aventi una certa dimensione in byte. Il processore
è schematizzabile, per i nostri scopi, come un elemento che,
dietro i comandi impartiti da una logica di controllo, preleva
i dati dalla memoria centrale (fase di read o load),
li elabora (fase di execute), e una volta processati
scrive i risulati finali (fase di write back o store)
nuovamente in memoria. Questo schema descrive l'arcinota macchina
di Von Neumann, dal nome del cervellone che ideò lo schema
base del funzionamento di un calcolatore negli anni Quaranta
. Potete pensare alla macchina di Von Neumann come ad un tizio
con un braccio solo: può eseguire una sola operazione alla volta
, prelevando documenti da uno scaffale, riordinandoli, e riporli
nuovamente nello scaffale una volta riordinati. La figura sottostante
chiarisce le idee :

Il
problema di una tale soluzione è che il concetto di memoria
è molto "nebuloso": un dispositivo di memorizzazione può essere
un registro (che funziona alla velocità di clock del processore
in cui è inglobato), una cache, una RAM di sistema o ,alla peggio,
l'intero Hard Disk!
La maggior parte delle istruzioni CISC fanno uso di
modalità di indirizzamento complesso sempre nell'ottica di ridurre
la dimensione del codice compilato. Un esempio banale per capire
l'approccio CISC all'uso della memoria: abbiamo due valori immagazzinati
in due celle di memoria, esempio la cella numero 100 e la cella
numero 230. Dobbiamo fare la moltiplicazione fra i contenuti
delle due celle e scrivere il risulato nella cella numero 300.
Il modo di procedere corretto sarebbe allora questo:
Codice
assembler per l'operazione di moltiplicazione fra due valori
immagazzianti in memoria centrale:
MOV A, %100;
muovi (move) nel registro A il contenuto della cella 100
MOV B,%230;
salva in un altro registro, detto B, il contenuto della cella
230
MUL C,A,B; moltiplica
A per B e scrivi il risultato nel registro C
STR C, %300; scrivi
(store) il valore di C nella cella numero 300
Osserviamo che, in totale,
abbiamo 4 istruzioni in assembler. Troppe per i progettisti,
secondo la filosofia CISC. Perchè non fare una singola istruzione
di moltiplicazione che preveda tutte queste operazioni una volta
decodificata dal processore? Se venisse aggiunta nell'ISA del
processore, basterebbe scrivere:
MUL %300,%230,%100
per impartire l'ordine al processore di salvare il contenuto
della memoria nei registri, fare la moltiplicazione e scrivere
il risualtato nuovamente in memoria. Bel risparmio di mal di
testa per i programmatori in assembler! E bel risparmio per
la ditta produttrice di software che impiegherà molto meno tempo
per correggere eventuali bachi nel programma, portando al pubblico
il prodotto finito in tempi più rapidi!
Purttoppo, come si sul dire, non è tutto oro quello
che luccica....