Sei sulla pagina 1di 35

Ap p u n t i d i Ca lco la to ri E lettro nici

C a p i t o l o 3 – I n s i e me d i i s t r u z i o n i

C ONCETTI INTRODUTTIVI .................................................................................................... 2


I formati delle istruzioni ................................................................................................. 2
Criteri generali di progettazione dei formati delle istruzioni ............................................. 2
Cenni all’indirizzamento................................................................................................. 4
Indirizzamento immediato ........................................................................................... 4
Indirizzamento diretto ................................................................................................. 4
Indirizzamento tramite registri ..................................................................................... 5
Indirizzamento indiretto .............................................................................................. 5
ARCHITETTURE DEGLI INSIEMI DI ISTRUZIONI ....................................................................... 7
Introduzione .................................................................................................................. 7
Classificazione delle architetture degli insiemi di istruzioni .............................................. 7
Esempio: istruzione di somma...................................................................................... 9
Vantaggi e svantaggi delle varie alternative ................................................................ 10
Collocazione in memoria degli operandi ........................................................................ 11
Classificazione delle macchine a registri generali ........................................................ 12
Indirizzamento della memoria ....................................................................................... 14
Interpretazione degli indirizzi di memoria................................................................... 14
Problemi di disallineamento ................................................................................... 18
Dispositivo di allineamento .................................................................................... 22
Modalità di indirizzamento ........................................................................................ 23
Frequenze relative di utilizzo delle varie modalità di indirizzamento ............................ 26
Modalità di indirizzamento con spiazzamento ............................................................. 27
Modalità di indirizzamento immediato........................................................................ 27
Codifica delle modalità di indirizzamento................................................................... 28
Operandi dell’insieme di istruzioni ................................................................................ 29
Istruzioni per il controllo del flusso............................................................................ 30
L’importanza dello zero nei test di uguaglianza........................................................ 32
Ulteriori considerazioni.......................................................................................... 32
Tipo e dimensione degli operandi .................................................................................. 33
Linguaggio di descrizione dell’hardware ....................................................................... 35
Appunti di “Calcolatori elettronici” – Capitolo 3

C
Coon
ncceettttii iin
nttrro
oddu
uttttiivvii

I formati delle istruzioni


Un programma è costituito da una sequenza di istruzioni, ognuna delle quali
specifica una determinata azione. La prima parte di ogni istruzione prende il nome di
codice operativo e serve appunto a specificare quale azione si deve eseguire.
Successivamente, la gran parte delle istruzioni contengono i dati da utilizzare oppure
indicano la “locazione” di tali dati; in ogni caso, si parla di operandi: ad esempio,
una istruzione che confronta due caratteri per vedere se sono uguali, deve
specificare quali sono i caratteri da confrontare. Tutto ciò che riguarda la
specificazione di dove si trovano gli operandi da usare prende il nome di
indirizzamento.

Formato generico di una istruzione

Codice Operativo Indirizzamento (operandi)

A seconda della macchina presa in considerazione, le istruzioni possono avere


tutte la stessa lunghezza (caso raro) oppure lunghezze diverse (caso più frequente).
Inoltre, in generale, le istruzioni possono essere più corte, uguali o più lunghe di una
parola.

Criteri generali di progettazione dei formati delle


istruzioni
Dovendo scegliere il formato (o i formati) delle istruzioni di una macchina, i fattori
da considerare sono molteplici. Il principio forse più importante da tenere presente è
che le istruzioni brevi sono migliori di quelle più lunghe , per una serie di motivi:

• un programma costituito da N istruzioni a 16 bit occupa solo la metà dello


spazio di memoria di un programma con N istruzioni a 32 bit. Un primo motivo
importante riguarda perciò l’occupazione di memoria;
• un secondo motivo è legato al fatto che ogni memoria ha una particolare
velocità di trasferimento (misurata in bps, ossia bit al secondo), determinata
dalla tecnologia e dalla progettazione della memoria stessa ( 1), e tale velocità
può influenzare la velocità di esecuzione delle istruzioni: se la velocità di
trasferimento di una memoria è di T bps e se la lunghezza media delle
istruzioni è di R bit, allora la memoria potrà trasportare al massimo T/R
istruzioni al secondo verso il processore e questo valore influenzerà la velocità
di esecuzione delle istruzioni; in particolare, se il tempo richiesto dal solo

1
La velocità di trasferimento di una memoria è il numero di bit al secondo che si possono leggere dalla
memoria stessa, per cui una memoria veloce può offrire al processore (oppure ad un dispositivo di I/O) più bit
al secondo di una memoria lenta.

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


2
Progetto dell’insieme di istruzioni

processore per eseguire una istruzione (una volta cioè che questa è stata
prelevata dalla memoria) è lungo a confronto con il tempo di prelevamento,
allora la velocità della memoria è meno importante e quindi lo stesso vale per la
lunghezza delle istruzioni; al contrario, con le CPU veloci, la memoria è spesso
il collo di bottiglia, per cui aumentare il numero di istruzioni prelevate al
secondo può essere di importanza fondamentale;
• un altro criterio di progettazione è che le istruzioni devono avere uno “spazio”
sufficiente per esprimere tutte le operazioni desiderate: è impossibile avere una
macchina che può compiere 2 N istruzioni ma con istruzioni più corte di N bit,
proprio perché non ci sarebbe abbastanza spazio nel codice operativo per
indicare quale operazione è necessaria;
• è inoltre auspicabile che la lunghezza della parola sia un multiplo intero della
lunghezza di carattere: se il codice del carattere ha k bit, la lunghezza della
parola dovrebbe essere k, 2k, 3k, 4k e così via; in caso contrario, sarebbe
sprecato dello spazio quando vengono memorizzati dei caratteri.
Questi limiti imposti sulla lunghezza della parola dal codice del carattere
influiscono evidentemente sulla lunghezza delle istruzioni: infatti, una istru_
zione può occupare un numero intero di byte o parole oppure un numero
intero di istruzioni deve essere contenuto in una parola;
• un ultimo criterio riguarda il numero di bit corrispondenti ad un campo di
indirizzamento. Per comprendere questo criterio, si ritiene opportuno fare un
esempio: consideriamo il progetto di una macchina in cui un carattere abbia
lunghezza 8 bit e in cui la memoria centrale debba avere capacità di 2 16
caratteri; il progettista potrebbe allora scegliere di assegnare indirizzi
consecutivi alle unità di 8 bit oppure di 16 bit oppure di 24 bit o di 32 bit o
altro. Ad esempio, supponiamo che si debba scegliere tra unità da 8 bit e unità
da 32 bit: nel primo caso, si dovrebbe avere una memoria da 2 16 byte (numerati
da 0 a 65535), mentre nel secondo caso di dovrebbe avere una memoria da 2 14
parole (numerate da 0 a 16383). Scegliendo l’indirizzamento delle parole da 32
bit, ci sarebbe il problema che il programma dovrebbe non solo prelevare le
parole contenenti i caratteri, ma anche estrarre il carattere da ciascuna parola,
richiedendo perciò un numero maggiore di istruzioni, con conseguente aumento
sia dello spazio occupato dal programma sia del tempo di esecuzione; al
contrario, l’organizzazione a 8 bit fornirebbe un indirizzo per ogni carattere,
facilitando il tutto. D’altra parte, a parità di capacità complessiva di memoria,
la scelta dei 32 bit richiederebbe indirizzi a 14 bit, mentre invece la scelta degli
8 bit richiederebbe indirizzi a 16 bit; avere indirizzi più brevi significa istruzioni
più brevi, il che a sua volta comporta sia meno spazio occupato dai programmi
sia anche meno tempo di prelevamento dalla memoria. Non solo, ma volendo
adottare indirizzi da 16 bit anche quando si indirizzano parole da 32 bit, si
otterrebbe un aumento della capacità della memoria di un fattore 4 rispetto a
quella permessa dall’organizzazione ad 8 bit. Questo esempio aiuta dunque a
capire che, per ottenere una migliore risoluzione di memoria (ossia la
possibilità di indirizzare “pezzi” più piccoli della memoria), bisogna pagare il
prezzo di indirizzi più lunghi, che, in generale, significano anche istruzioni più
lunghe.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


3
Appunti di “Calcolatori elettronici” – Capitolo 3

Cenni all’indirizzamento
E’ possibile classificare le istruzioni di un calcolatore a seconda del numero di
operandi che usano. A tal proposito, è subito importante precisare che un gruppo di
registri (numerati) della CPU costituisce una memoria ad alta velocità e definisce
quindi uno spazio di indirizzamento che si affianca a quello costituito dalla memoria
vera e propria. Quindi, una istruzione che somma il contenuto del registro R1 con
quello del registro R2 deve essere classificata come una istruzione con due operandi,
dato che essa deve specificare quali registri sommare esattamente nello stesso modo
in cui una istruzione che somma due parole di memoria deve specificare quali parole.
Le istruzioni più comuni sono quelle che specificano uno, due o anche tre
operandi . Su molte macchine, del resto, si usa un unico operando, in quanto uno
speciale registro, detto accumulatore, fornisce l’altro operando: si tratta delle
cosiddette architetture ad accumulatore. Su queste macchine, l’indirizzo
specificato è di solito quello di una parola di memoria m in cui si trova l’operando.
Ad esempio, l’istruzione per l’addizione che specifica l’indirizzo m ha il seguente
effetto:
accumulatore := accumulatore + memoria[m]

Le istruzioni di somma a due operandi usano invece un operando per la sorgente e


l’altro per la destinazione, per cui la sorgente viene sommata alla destinazione:

destinazione := destinazione + sorgente

Le istruzioni a tre operandi specificano infine due sorgenti ed una destinazione:

destinazione := sorgente1 + sorgente2

Premesse queste considerazioni generali, dobbiamo prestare maggiore attenzione a


come i bit del campo di indirizzamento vengono interpretati per trovare l’operando
cui fanno riferimento. Il caso più intuitivo è quello in cui essi contengono l’indirizzo
di memoria dell’operando, ma ci sono una serie di alternative che esamineremo nei
prossimi paragrafi.

nd
IIn meen
diirriizzzzaam mm
nttoo iim diiaattoo
meed
Il modo più semplice per specificare un operando in una istruzione è che la parte
di indirizzamento dell’istruzione contenga effettivamente l’operando stesso invece di
un indirizzo o di qualunque altra informazione che descriva la posizione
dell’operando. Si parla in questo caso di indirizzamento immediato e operando
immediato, proprio perché quest’ultimo viene prelevato dalla memoria insieme
all’istruzione stessa ed è dunque subito disponibile all’uso.
L’indirizzamento immediato ha l’indubbio pregio di non richiedere un accesso alla
memoria aggiuntivo per prelevare l’operando. Ha però anche il difetto di limitare il
valore dell’operando ad un numero che sia contenuto nel campo di indirizzamento:
se tale campo è lungo N bit, l’operando potrà valere al più 2 N -1.

IIn
nddiirriizzzzaam
meen diirreettttoo
nttoo d
In questo caso, la posizione dell’operando viene specificata indicando l’indirizzo
della parola di memoria che lo contiene. Senza scendere, per ora, nei dettagli di come

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


4
Progetto dell’insieme di istruzioni

un calcolatore possa riconoscere gli operandi immediati da quelli diretti, ci limitiamo


a dire che esistono due possibili strategie: usare codici operativi differenti oppure
usare speciali modi di indirizzamento per ogni tipo di operando.

IIn
nddiirriizzzzaam
meen
nttoo ttrraam
miittee rreeggiissttrrii
L’indirizzamento tramite registri è concettualmente uguale all’indirizzamento
diretto, in quanto il campo di indirizzamento contiene il numero (detto numero
d’ordine) del registro in cui è contenuto l’operando. D’altra parte, abbiamo già
osservato che una macchina dotata di registri ha in effetti due “spazi di
indirizzamento”, uno corrispondente appunto ai registri e l’altro alla memoria; di
conseguenza, si può pensare ad un indirizzo su tale macchina come composto da
due parti: un bit viene usato per indicare se si desidera un registro oppure una
parola di memoria, mentre i restanti bit forniscono, rispettivamente, il numero
d’ordine del registro o l’indirizzo della parola di memoria.
E’ ovvio, però, che i registri sono molti meno delle parole di memoria, per cui l’uso
di un campo di indirizzamento di uguale lunghezza nei due casi sarebbe uno spreco:
vengono quindi generalmente usate istruzioni di diverso formato per operandi di
registro e operandi di memoria. Se ci fosse una istruzione di registro per ogni
istruzione che indirizza la memoria, avremmo metà dei codici operativi per gli
operandi di registro e metà per gli operandi di memoria; la distinzione tra i codici
operativi avverrebbe tramite un bit per designare quale spazio di indirizzamento
usare. In alternativa, tale bit potrebbe essere spostato nel campo di indirizzamento.
Le macchine sono progettate con registri per due fondamentali ragioni:

• i registri sono più veloci della memoria centrale;


• essendo pochi, i registri richiedono meno bit per essere identificati.

Il problema viene invece dal numero di registri, che complica non poco la
programmazione in quanto bisogna prendere delle decisioni su quali operandi e su
quali risultati intermedi devono essere contenuti nei pochi registri a disposizione e
su quali invece nella memoria centrale.

nd
IIn diirriizzzzaam nd
nttoo iin
meen diirreettttoo
Abbiamo detto che l’indirizzamento diretto è uno schema in cui l’indirizzo
contenuto nell’istruzione specifica quale parola di memoria o quale registro contiene
l’operando. Al contrario, l’indirizzamento indiretto è uno schema in cui l’indirizzo
contenuto nell’istruzione specifica quale parola di memoria o quale registro contiene
non l’operando, ma l’indirizzo dell’operando. Ad esempio, consideriamo una
istruzione che serve a caricare il registro R1 tramite l’operando indiretto contenuto
nella locazione 1510 “puntata” dalla locazione 1000:

• per prima cosa, il contenuto della locazione 1000 viene copiato in un


registro interno della CPU;
• tale contenuto (1510) viene assunto non come operando (come avverrebbe
nel caso di indirizzamento diretto), ma come indirizzo della locazione
contenente effettivamente l’operando: l’operando viene quindi prelevato dalla
locazione 1510 e portato in R1. Il contenuto della locazione 1000 viene detto
puntatore, per evidenti motivi.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


5
Appunti di “Calcolatori elettronici” – Capitolo 3

Alcune macchine permettono anche l’indirizzamento indiretto a più livelli: si


tratta, in pratica, del meccanismo per cui un puntatore viene usato per trovare una
parola di memoria che punta a sua volta ad un’altra parola e così via.
E’ interessante notare che gli indirizzamenti immediato, diretto, indiretto e
indiretto a più livelli mostrano una “progressione” nel numero di accessi alla memoria
(intendo con questa espressioni anche gli accessi a registro):

• l’indirizzamento immediato no richiede accessi alla memoria, dato che l’


operando è prelevato insieme all’istruzione;
• l’indirizzamento diretto richiede un accesso alla memoria, per prelevare
l’operando;
• l’indirizzamento indiretto richiede due accessi alla memoria, uno per il
puntatore e l’altro per l’operando;
• l’indirizzamento indiretto a più livelli richiede almeno tre accessi alla
memoria, due o più per i puntatori e l’ultimo per l’operando vero e proprio.

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


6
Progetto dell’insieme di istruzioni

hiitteettttu
Arrcch
A urree d gllii iin
deeg dii iissttrru
mii d
nssiieem on
uzziio nii

Introduzione
L’argomento di cui intendiamo occuparci, a partire da ora e proseguendo nel
prossimo capitolo, è l’architettura dell’insieme di istruzioni (Instruction Set) di una
macchina, ossia quella parte della macchina che è “visibile” sia al programmatore sia
al progettista di compilatori. In particolare, descriveremo l’ampia varietà di
alternative di progetto possibili per l’insieme di istruzioni.
L’architettura dell’insieme di istruzioni è, di fatto, lo “specchio” dell’architettura
del calcolatore: essa infatti indica ciò che l’architettura consente al programmatore e
come le varie funzioni consentite sono realizzate.
Ci concentreremo specificamente su due aspetti principali:

• nella prima parte, faremo una panoramica dei possibili insiemi di istruzioni,
fornendo una serie di giudizi qualitativi su vantaggi e svantaggi legati a
ciascuna alternativa;
• successivamente, presenteremo alcune misure di prestazioni, aventi la
caratteristica di essere indipendenti da uno specifico insieme di istruzioni;

A proposito delle misure di prestazioni, è importante specificare che esse,


mentre non dipendono da uno specifico insieme di istruzioni, dipendono dal
programma da esaminare nonché dal compilatore usato nelle prove . Questo
comporta che i relativi risultati non debbano essere interpretati in senso assoluto: se
le stesse valutazioni fossero effettuate con un altro compilatore e/o con programmi
differenti, i risultati ottenuti sarebbero diversi.
I dati che presenteremo si riferiscono ad un piccolo insieme di benchmark ( 2):
questo consente di mostrare, in modo semplice, sia i dati in se stessi sia le differenze
rilevate tra i vari programmi. E’ ovvio, del resto, che, dovendo progettare una
macchina nuova, è necessario analizzare un insieme di programmi ben più ampio di
quello da noi usato prima di decidere quale architettura scegliere.
Tutte le misure che effettueremo saranno di tipo dinamico, nel senso che la
frequenza di un dato evento verrà pesata dal numero di volte in cui l’evento stesso si
verifica durante l’esecuzione del programma in esame ( 3).

Classificazione delle architetture degli insiemi di


istruzioni
Ci sono varie possibilità per classificare gli insiemi di istruzioni. La prima che
vogliamo esporre si basa sui seguenti cinque aspetti, elencati in ordine decrescente
di importanza:

2
Si veda, in proposito, quanto detto sui benchmark nel capitolo 2.
3
Anche quest’ultimo concetto è stato già discusso nel capitolo 2.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


7
Appunti di “Calcolatori elettronici” – Capitolo 3

• memorizzazione degli operandi nella CPU: bisogna valutare in quali altri


“posti”, oltre che nella memoria centrale, vengono memorizzati gli operandi
delle istruzioni;
• numero di chiamate esplicite agli operandi per istruzione: è noto che le
istruzioni possono operare su operandi espliciti (dei quali venga cioè
indicata espressamente la “posizione”) e/o su operandi impliciti (la cui
“posizione” sia cioè nota a priori, in base al codice operativo delle istruzioni
stesse); è allora importante valutare il numero medio di operandi impliciti o
espliciti usati nelle istruzioni;
• collocazione operandi per le istruzioni aritmetico-logiche: bisogna inoltre
chiedersi se sia possibile che qualche operando di una istruzione aritmetico-
logica si trovi in memoria oppure se tutti gli operandi debbano trovarsi nei
registri interni della CPU; non solo, ma, nel caso in cui un operando si trovi
in memoria, bisogna capire come venga specificata la sua posizione;
• operazioni: è ovvia anche l’importanza di conoscere quali operazioni siano
previste dall’insieme di istruzioni;
• tipo e dimensione degli operandi: infine, è importante sapere quali sono i tipi
di operandi usati nelle istruzioni, la dimensione di ciascuno di essi ed il
modo in cui vengono specificati.

I prossimi paragrafi analizzano uno ad uno questi cinque aspetti. Il primo aspetto,
ossia il tipo di memorizzazione fornita per contenere gli operandi nella CPU, è il
principale elemento di distinzione tra le architetture degli insiemi di istruzioni. La
sua importanza deriva anche dal fatto che la stragrande maggioranza delle
architetture prevedono una memorizzazione temporanea all’interno della CPU. Non
solo, ma il tipo di memorizzazione degli operandi nella CPU talvolta vincola anche il
numero di operandi richiamabili in modo esplicito da una istruzione (secondo aspetto).
Essendo dunque il criterio principale di classificazione, il tipo di memorizzazione
degli operandi nella CPU sarà il criterio base con cui esamineremo le diverse
alternative possibili. A tal proposito, riportiamo subito una tabella che elenca le
principali tre alternative possibili:

Tipo di
Operandi espliciti
memorizzazione Destinazione dei Procedura per accedere in
per istruzione
nella CPU risultati modo esplicito agli operandi
dell’ALU
Stack (pila) 0 Stack Push e pop sulla e dalla pila
Caricamento/memorizzazione
Accumulatore 1 Accumulatore
con accumulatore
Insieme dei Caricamento/memorizzazione
2o3 Registri o memoria
registri dei registri di memoria

Sono dunque considerate tre alternative:

• architettura a pila: in essa gli operandi sono tutti impliciti e si trovano in


cima alla pila;

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


8
Progetto dell’insieme di istruzioni

• architettura con accumulatore: in questo caso, uno degli operandi è


sempre implicitamente il contenuto dell’accumulatore;
• architettura a registri di uso generale: in questo caso si usano solo
operandi espliciti, situati sia nei registri della CPU sia nelle locazioni di
memoria.

E’ evidente che ogni alternativa influenza il numero di operandi espliciti per quelle
istruzioni che prevedono due operandi di partenza ed un risultato:

• nell’architettura a pila, entrambi gli operandi di partenza si trovano nella


pila (impliciti), dalla quale vengono prelevati con la classica operazione di
pop (estrazione); una volta ricavato il risultato, esso viene inserito nella pila,
con la classica operazione di push (inserimento);
• nell’architettura ad accumulatore, uno dei due operandi è implicitamente
l’accumulatore, mentre l’altro deve essere indicato esplicitamente; una
volta ricavato il risultato, esso va posto nuovamente nell’accumulatore;
• infine, nell’architettura a registri, è necessario indicare quantomeno due dei
tre operandi in modo esplicito; nel caso si indichino solo due operandi, la
posizione del terzo è implicita e dipende dall’istruzione specifica (quindi dal
suo codice operativo): generalmente si tratterà del contenuto di uno
specifico registro.

E
Esseem
mppiioo:: iissttrru
uzziioon
nee d
dii ssoom
mmmaa
Per comprendere ancora meglio i concetti legati alla memorizzazione degli operandi
in una architettura, consideriamo una semplice istruzione di somma (add):

C = A + B

Vediamo come questa istruzione venga “tradotta” a seconda del tipo di architettura
dell’insieme di istruzioni:

• architettura a pila: in questo caso, bisogna per prima cosa prelevare i due
operandi dallo stack, dopodiché se ne potrà fare la somma e depositare il
risultato nuovamente nello stack; si ha perciò il seguente codice:

PUSH A
PUSH B
ADD
POP C

• architettura ad accumulatore: in questo caso, uno dei due operandi si trova


nell’accumulatore, mentre l’altro deve essere specificato in modo esplicito;
bisogna perciò prelevare l’operando esplicito e farne la somma con
l’accumulatore, riportando il risultato nell’accumulatore stesso; si ha perciò
il seguente codice:
LOAD A
ADD B
STORE C

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


9
Appunti di “Calcolatori elettronici” – Capitolo 3

• architettura a registri: in questo caso, supponiamo che i due operandi A e B


si trovino entrambi in memoria e che il risultato della loro somma debba
essere memorizzato a sua volta in memoria; è necessario allora usare un
registro (ad esempio R1) per accogliere sia uno dei due operandi sia il
risultato della somma, in modo da ottenere il seguente codice:

LOAD R1,A
ADD R1,B
STORE C,R1

La prima istruzione carica A nel registro R1; la seconda somma il


contenuto del registro R1 con il valore dell’operando B e pone il risultato
ancora in R1; l’ultima istruzione memorizza il valore di R1 nella locazione
dell’operando C. Così facendo, nessuno degli operandi di partenza viene
“distrutto” (cioè perso).

Una osservazione viene spontanea a questo punto: quando ci troviamo davanti ad


una istruzione ma non sappiamo a quale macchina “appartenga”, il semplice formato
di tale istruzione potrebbe essere indicativo del tipo di macchina: ad esempio, se
abbiamo una istruzione del tipo ADD A, in cui si fa riferimento ad una somma e si
indica un unico operando, è molto probabile che la macchina di riferimento abbia
una architettura ad accumulatore: l’operando specificato deve essere sommato al
contenuto dell’accumulatore ed il risultato andrà a sua volta nell’accumulatore. Se
invece l’istruzione fosse ADD R1,B , allora con molta probabilità essa si riferirebbe ad
una architettura a registri, in quanto vengono specificati due operandi, di cui il
primo (un registro) fa sia da sorgente sia da destinazione. Se infine l’istruzione fosse
semplicemente ADD, allora non ci sarebbero dubbi: si tratterebbe di una architettura
a pila, in quanto la mancanza di operandi espliciti significa che la posizione di tali
operandi è implicita ed è appunto lo stack.

V
Vaan
nttaaggggii ee ssvvaan
nttaaggggii d
deellllee vvaarriiee aalltteerrn
naattiivvee
Alla luce dell’esempio del paragrafo precedente, possiamo evidenziare vantaggi e
svantaggi principali delle tre alternative:

Tipo di macchina Vantaggi Svantaggi


Usa un modello semplice per la La pila non permette l’accesso casuale
valutazione delle espressioni: e questa limitazione rende difficile la
notazione polacca inversa. generazione di codice efficiente.
Pila Le istruzioni corte possono E’ anche difficile realizzare in modo
produrre buona densità di codice efficiente queste architetture, in quanto
la pila diviene un collo di bottiglia
Minimizza gli stati interni della Il traffico di memoria diventa elevato,
Accumulatore macchina e consente di usare dato che l’accumulatore è solo una
istruzioni corte memoria temporanea
E’ il modello più generale possibile Tutti gli operandi devono essere
Registro per la generazione del codice specificati, dando origine ad istruzioni
più lunghe rispetto agli altri casi

Vantaggi e svantaggi elencati in questa tabella sono dunque essenzialmente legati


a tre aspetti:

• in quale misura la macchina si adatta alle necessità del compilatore;


• efficienza dal punto di vista realizzativo;

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


10
Progetto dell’insieme di istruzioni

• dimensione del codice.

Mentre le prime macchine usavano architetture a pila oppure ad


accumulatore, tutte le macchine progettate negli ultimi anni usano invece
l’architettura a registri, che è senz’altro quella di uso più generale . I motivi di
questo successo sono essenzialmente due:

• prima di tutto, i registri, come altre forme di memorizzazione interna alla


CPU, sono molto più veloci della memoria;
• inoltre, i registri sono più facili da usare e possono quindi essere sfruttati in
modo più efficiente rispetto ad altre forme di memorizzazione interna.

Data questa prevalenza dell’uso dei registri, i prossimi discorsi faranno riferimento
solo a questo tipo di architettura, tralasciando le altre due. Segnaliamo comunque
che le architetture a pila hanno comunque una buona applicazione al giorno d’oggi,
essenzialmente grazie alla loro grande semplicità (in termini di realizzazione e quindi
anche di costo). Tra l’altro, tutte le calcolatrici sono in pratica dei miniprocessori con
architettura a pila: infatti, lo stack è la struttura astratta di dati più idonea per
implementare la notazione polacca, notoriamente usata per il calcolo automatico
delle espressioni matematiche. Le architetture ad accumulatore sono anch’esse
molto semplici e, allo stesso tempo, efficienti ed è stato infatti per questo che esse
hanno determinato il vero boom dei microprocessori. Una tipica applicazione, al
giorno d’oggi, delle architetture ad accumulatore sono i registratori di cassa.

Collocazione in memoria degli operandi


Con il paragrafo precedente abbiamo sostanzialmente esaminato i primi due
aspetti legati alla classificazione degli insiemi di istruzioni, vale a dire la “posizione”
in cui possono essere memorizzati gli operandi oltre che in memoria ed il numero di
chiamate esplicite agli operandi per istruzione. Vogliamo ora occuparci del terzo
aspetto, ossia della collocazione in memoria degli operandi.
A tal proposito, il vantaggio principale delle macchine a registri per uso
generale (general purpose registers) è il fatto che i compilatori riescono ad usare
tali registri in modo molto efficace, sia quando devono calcolare i valori delle
espressioni sia, più in generale, quando devono usarli per contenere delle variabili.
Per quanto riguarda il primo aspetto, i registri consentono di “ordinare”, in modo
più flessibile rispetto ad altri tipi di architetture, le espressioni da valutare. Per
comprendere questo concetto, consideriamo la seguente espressione matematica:

(A×B) – (C×D) – (E×F)

E’ noto che le prime operazioni da compiere sono le tre moltiplicazioni, dopodiché


si potranno effettuare le due sottrazioni. Allora, usando una macchina a registri, è
possibile effettuare le moltiplicazioni in un ordine qualunque, il che permette di
ottimizzare il processo in base alla posizione particolare degli operandi oppure in
base alla presenza di una pipeline (di cui parleremo in un prossimo capitolo). Al
contrario, se avessimo una macchina a pila, saremmo costretti a valutare
l’espressione da sinistra verso destra, senza poter fare operazioni speciali e/o scambi
di posizione nella pila stessa.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


11
Appunti di “Calcolatori elettronici” – Capitolo 3

Ancora più importante è il ruolo dei registri per memorizzare le variabili; infatti,
quando le variabili sono allocate nei registri, si ottengono vari vantaggi:

• si riduce il traffico di memoria;


• il programma risulta più veloce, in quanto i registri sono più veloci della
memoria;
• aumenta la densità del codice, dato che un registro, rispetto ad una
locazione di memoria, può essere identificato con un numero inferiore di bit.

In base a queste considerazioni, la situazione ottimale sarebbe quella per cui


tutti i registri di una data macchina fossero di uso generale, non riservati cioè a
scopi particolari . Questo purtroppo non sempre avviene, specialmente in alcune
vecchie macchine dove erano previsti molti registri, la maggior parte dei quali
dedicati a scopi specifici: se il numero di registri di uso generale è troppo piccolo,
potrebbe addirittura non essere conveniente allocare le variabili nei registri, mentre
invece un compilatore dovrebbe comunque continuare ad usare i registri generali per
la valutazione delle espressioni.
Sorge allora il problema di stimare quale sia il numero sufficiente di registri
generali. La risposta non può ovviamente essere univoca, ma dipende dal modo in
cui il compilatore usa tali registri: nella maggior parte dei casi, i compilatori usano
alcuni registri per la valutazione delle espressioni e altri per il passaggio dei
parametri, lasciando liberi i registri rimanenti per l’allocazione delle variabili; di
conseguenza, per capire quanti registri sono necessari, è indispensabile esaminare
quali variabili possono essere allocate nei registri e quale algoritmo di allocazione
viene usato.

C
Cllaassssiiffiiccaazziioon
nee d
deellllee m
maacccch
hiin
nee aa rreeggiissttrrii ggeen
neerraallii
Le macchine a registri generali, oggetto dei nostri discorsi, possono essere a loro
volta classificate in base a due caratteristiche, relative entrambe alla natura degli
operandi per una tipica istruzione di tipo aritmetico-logico ( 4):

• la prima caratteristica riguarda la presenza di due soli operandi anziché tre:


nei formati a tre operandi, due sono ovviamente gli operandi di partenza
(operandi sorgente), mentre invece l’altro è il risultato (operando
destinazione); al contrario, nei formati a due operandi, uno di essi contiene
un operando di partenza ma verrà anche usato per contenere il risultato
finale;
• la seconda caratteristica è legata invece al numero di operandi da
indirizzare nelle istruzioni di ALU: il numero di operandi in memoria
consentiti varia tipicamente da 0 a 3.

E’ evidente, allora, che avendo 2 possibilità per la prima caratteristica e 4


possibilità per la seconda, i casi possibili sono in totale 8; in realtà, il caso con 3
indirizzamenti a memoria prevede necessariamente che anche il massimo numero di
operandi permessi sia 3, per cui i casi sono 7 e sono riportati nella tabella seguente:

4
Queste istruzioni sono anche dette istruzioni di ALU, per ovvi motivi.

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


12
Progetto dell’insieme di istruzioni

Numero di indirizzamento a memoria Massimo numero di operandi permessi


2
0
3
2
1
3
2
2
3
3
3

Pur essendoci 7 diverse possibilità, solo tre di esse permettono di classificare


quasi tutte le macchine esistenti, in quanto sono essenzialmente tre i tipi di
macchine usate al giorno d’oggi:

• macchine registro-registro (dette anche load/store, ossia


carica/memorizza): sono quelle che non ammettono riferimenti a memoria
per le istruzioni dell’ALU;
• macchine registro-memoria: sono quelle in cui almeno un operando è
contenuto in un registro;
• macchine memoria-memoria: sono quelle in cui tutti gli operandi sono in
memoria.

Vediamo allora quali sono i vantaggi e gli svantaggi principali di queste tre
alternative, precisando però subito che non si tratta di considerazioni di validità
assoluta:

Tipo di macchina Vantaggi Svantaggi


Codifica semplice di istruzioni a E’ più alto il numero di istruzioni
lunghezza fissa. rispetto ad architetture con riferimento
Modello semplice di generazione in memoria nelle istruzioni.
Registro-registro (0,3) del codice. Alcune istruzioni sono corte e la
Le istruzioni vengono eseguite in codifica a bit potrebbe essere
un numero di cicli di clock dispendiosa
comparabile.
Si può accedere ai dati senza Gli operandi non sono “equivalenti”,
prima caricarli. dato che uno degli operandi di
Il formato dell’istruzione tende ad partenza viene “distrutto” a seguito di
essere di facile codifica e consente una operazione binaria.
buone densità. Codificare un numero di registro ed un
indirizzo di memoria in ogni istruzione
Registro-memoria(1,2) può limitare il numero di registri.
Il numero di cicli di clock per istruzione
varia a seconda della locazione
dell’operando.
E’ il meccanismo più compatto. La dimensione delle istruzioni varia in
Non spreca registri per maniera frequente e di molto,
memorizzazioni temporanee. specialmente per istruzioni a 3
Memoria-memoria(3,3) operandi.
Si hanno grandi variazioni nel carico di
lavoro per istruzione.
Gli accessi a memoria creano un collo
di bottiglia nell’esecuzione.

In questa tabella, la notazione (m,n) usata nella prima colonna significa che si considerano m
operandi in memoria e n operandi in totale.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


13
Appunti di “Calcolatori elettronici” – Capitolo 3

Ancora una volta, notiamo che i vantaggi e gli svantaggi elencati nella tabella sono
legati essenzialmente all’impatto che le varie alternative hanno sia sul compilatore
sia sulla realizzazione delle macchine.
In generale, possiamo affermare che macchine con poche alternative semplificano
ovviamente il compito del compilatore, dato che le decisioni da prendere sono meno
numerose. Al contrario, le macchine con una grande varietà di istruzioni (con formati
flessibili) riducono il numero di bit richiesti per la codifica del programma e si parla
perciò di buona densità di istruzione: infatti, un piccolo numero di bit fa più lavoro
di uno più grande su una architettura differente. Inoltre, il numero di registri
influenza la dimensione delle istruzioni.

Indirizzamento della memoria


Vogliamo ora occuparci di due distinti argomenti, che prescindono entrambi dal
fatto che l’architettura del calcolatore sia a registri oppure permetta anche operandi
con riferimenti a memoria:

• il primo argomento è il metodo con cui devono essere interpretati gli


indirizzi di memoria;
• il secondo argomento è il metodo con cui tali indirizzi vengono specificati.

A ciascun argomento è dedicato un paragrafo.


Segnaliamo fin da ora che le analisi che presenteremo sono per lo più indipendenti
dalla macchina considerata, anche se non completamente. Inoltre, in alcuni casi tali
analisi sono condizionate in modo significativo dalla tecnica di compilazione. Nel
nostro caso particolare, si assume l’uso di un compilatore ottimizzante.

prreettaazziioon
ntteerrp
IIn nd
deeggllii iin
nee d dii m
diirriizzzzii d moorriiaa
meem
Ci chiediamo il modo in cui venga “interpretato” un indirizzo di memoria, ossia
sostanzialmente quale sia l’oggetto che viene referenziato dall’indirizzo e dallo
spiazzamento. Per rispondere a questa domanda, considereremo macchine la cui
memoria gode delle seguenti due caratteristiche:

• è organizzata a byte, ossia ogni byte della memoria è dotato di un proprio


indirizzo e quindi tutti gli indirizzi generati dalla CPU per accedere alla
memoria fanno riferimento a byte ( 5) ;
• è possibile indirizzare non solo i byte (8 bit), ma anche le parole corte (16
bit) e le parole (32 bit). In molti casi, sarà anche permesso l’indirizzamento
delle parole doppie (64 bit). ( 6)

5
E’ noto che, quando la CPU deve accedere ad un dato in memoria, genera l’indirizzo di tale dato e lo pone sul bus degli
indirizzi; l’unità di memoria, per esaudire la richiesta della CPU, legge tale indirizzo e legge anche un’altra linea del bus
(una linea di controllo), sulla quale la CPU indica se vuole leggere o scrivere qualcosa all’indirizzo specificato.
6
E’ importante comprendere la differenza tra quando parliamo di “memoria organizzata a byte” e quando invece parliamo
di “indirizzare un byte o una parola o altro”: avere una “memoria organizzata a byte” significa, come già detto, che ogni
byte ha un proprio indirizzo e quindi è univocamente individuabile (di riflesso, significa anche che il bus di indirizzo, ossia
quella parte del bus di sistema necessaria per individuare i dati cui accedere in memoria, individua sempre singoli byte);
quando invece parliamo di “indirizzare un byte o una parola o altro” intendiamo dire che le istruzioni possono specificare
alla CPU di considerare, come operando per una data operazione, un byte oppure una parola oppure un altro oggetto:

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


14
Progetto dell’insieme di istruzioni

La considerazione iniziale da fare è la seguente: i byte che costituiscono una


parola possono essere “numerati” da sinistra verso destra o viceversa. Questa
potrebbe apparire come una scelta irrilevante, ma in realtà ci si accorge presto che
non è così . Ad esempio, mentre nella famiglia Motorola (a 32 bit) i byte sono
normalmente numerati da sinistra a destra, al contrario, nella famiglia INTEL (a 32
bit) avviene il contrario, ossia la numerazione è da destra verso sinistra. Nel primo
caso di parla di calcolatori a finale grande (Big Endian) mentre nel secondo caso di
parla di calcolatori a finale piccolo (Little Endian).
La figura seguente riporta 4 parole della memoria di un calcolatore a 32 bit, i cui
byte sono numerati da sinistra a destra:

Indirizzo Finale grande


di parola
0 0 1 2 3
4 4 5 6 7
8 8 9 10 11
12 12 13 14 15

byte

Parola di 32 bit

In questo caso, l’indirizzo di parola (0,4,8 o 12 nel nostro esempio) identifica il


byte più a sinistra di ciascuna parola (byte più significativo).
La prossima figura riporta invece 4 parole della memoria di un calcolatore a 32
bit, i cui byte sono numerati da destra a sinistra:

Finale piccolo Indirizzo


di parola
3 2 1 0 0

7 6 5 4 4

11 10 9 8 8

15 14 13 12 12

byte

Parola di 32 bit

In questo caso, l’indirizzo di parola (sempre pari a 0,4,8 o 12) identifica il byte più
a destra di ciascuna parola (byte meno significativo). In definitiva, quindi,
nell’ordinamento di tipo Big Endian, l’indirizzo di un dato è quello del suo byte
più significativo, mentre invece, nell’ordinamento di tipo Little Endian,
l’indirizzo di un dato è quello del suo byte meno significativo .

l’indirizzo di tale “oggetto” corrisponde a quello del primo byte di tale oggetto, dopodiché la CPU saprà, in base al codice
operativo dell’istruzione, quanti altri byte, oltre quello iniziale, considerare come appartenenti a quell’oggetto; ad esempio,
una istruzione LW (load word) indica il caricamento di una parola da 32 bit dalla memoria in un registro interno alla CPU:
l’indirizzo della parola è quello del primo byte della parola stessa, dopodiché la CPU sa che deve prelevare non solo il
primo byte ma anche i 3 successivi.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


15
Appunti di “Calcolatori elettronici” – Capitolo 3

Naturalmente, però, i numeri vengono memorizzati sempre da destra verso


sinistra : per esempio, supponiamo di avere il numero intero 6, il quale, espresso in
binario con 32 bit, è

00000000 00000000 00000000 00000110

Nel caso del sistema a finale grande, questa configurazione viene inserita, così
com'è, a partire dal byte 0 per finire al byte 3. Al contrario, nel caso del sistema a
finale piccolo, il primo byte a sinistra va nel byte 3, il secondo a sinistra va nel byte
2, il successivo nel byte 1 e l'ultimo, quello che contiene i bit significativi, va nel
byte 0:

Big Endian
00000000 00000000 00000000 00000110
byte 0 byte 1 byte 2 byte 3

Little Endian
00000000 00000000 00000000 00000110
byte 3 byte 2 byte 1 byte 0

In ogni caso, i bit vengono comunque riportati con i tre bit 110 a destra della
parola, mentre i rimanenti 29 bit a sinistra sono tutti zeri.
Inoltre, in entrambi i casi, l'indirizzo della parola che contiene questo numero è
sempre 0. In un certo senso, quindi, cambia l'ordine con cui i bit vengono "letti":
mentre nel finale grande si legge a partire dalla cella avente l'indirizzo
specificato e si va per indirizzi crescenti, nel finale piccolo si fa il contrario .
Detto questo, vediamo perché l’ordinamento dei byte è un aspetto tutt’altro che
trascurabile. Per semplicità, consideriamo una semplice “scheda personale” di un
impiegato, costituita da una stringa (il nome dell’impiegato) e da due interi (l’età ed
il numero del dipartimento di appartenenza):

Nome: Jim Smith


Età: 21 anni
Dipartimento: 260 (1×256+4=260)

Dovendo memorizzare questa scheda, abbiamo bisogno di 9 byte per il nome (ogni
byte corrisponde ad un carattere, incluso lo spazio), di 1 byte per l’età e di 2 byte per
il numero di dipartimento. Se poi vogliamo far riferimento a tali informazioni non a
livello di singoli byte, ma a livello di singole parole (ad esempio da 32 bit), avremo
bisogno di inserire degli 0 non significativi per riempire tali parole. Ad esempio, la
rappresentazione a finale grande è mostrata nella figura seguente:

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


16
Progetto dell’insieme di istruzioni

Finale grande
0 J I M
4 S M I T
8 H 0 0 0
12 0 0 0 21
16 0 0 1 4

Se invece vogliamo la rappresentazione a finale piccolo, abbiamo quanto segue:

Finale piccolo
Indirizzo

M I J 0

T I M S 4

0 0 0 H 8

0 0 0 21 12

0 0 1 4 16

E’ chiaro che entrambe le rappresentazioni sono efficaci. Il problema nasce


invece quando una delle due macchine cerca di mandare la scheda ad un’altra
macchina appartenente alla stessa rete. Ad esempio, supponiamo che quella a
finale grande spedisca la scheda a quella a finale piccolo, trasmettendo un byte alla
volta, cominciando dal byte 0 e finendo con il byte 19 ( 7). In questa situazione, il
byte 0 della numerazione a finale grande va nel byte 0 della memoria a finale
piccolo, il byte 1 della numerazione a finale grande va nel byte 1 della memoria a
finale piccolo e così via, dando origine alla seguente situazione nella memoria della
macchina a finale piccolo:

Trasferimento da
finale grande a
finale piccolo
Indirizzo
M I J 0

T I M S 4

0 0 0 H 8

21 0 0 0 12

4 1 0 0 16

Il problema sorto è evidente: quando la macchina a finale piccolo tenta di


stampare la scheda, tutto funziona bene per il nome, mentre invece si ottiene una
età di 21×2 24 anni e l’indicazione del dipartimento è altrettanto erronea. Il motivo è
che la trasmissione ha invertito l’ordine dei caratteri in tutte le parole, ma solo
nelle prime tre questa operazione era lecita (trattandosi di stringhe), mentre invece
non lo era nelle altre due (contenenti numeri interi).

7
Per semplicità, supponiamo che i bit dei byte non vengano invertiti durante la trasmissione.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


17
Appunti di “Calcolatori elettronici” – Capitolo 3

Una soluzione ovvia del problema sarebbe quella di invertire, via software, i byte
di una parola dopo aver fatto la copia:

Trasferimento da
finale grande a
finale piccolo con scambio
Indirizzo

J I M 0

S M I T 4

H 0 0 0 8

0 0 0 21 12

0 0 1 4 16

Il problema, questa volta, è che i numeri interi sono giusti, ma non lo sono
le stringhe: infatti, quando il calcolatore legge la stringa iniziale, legge prima il byte 0
(che contiene uno spazio), poi il byte 1 (=M) e così via, commettendo ancora una
volta un errore.
Si intuisce come una soluzione semplice al problema non ci sia. Una soluzione
possibile, ma anche poco efficiente, potrebbe essere quella di includere un header
(testata), all’inizio di ogni record, che specifichi che tipo di dati seguono (stringhe,
interi, altri) e quanto sono lunghi; in tal modo, il calcolatore ricevente sa quando
eseguire gli scambi e quando no.

Problemi di disallineamento
Consideriamo una memoria organizzata a byte: questo significa che le celle di
memoria (ciascuna rappresentativa di un bit) sono “logicamente raggruppate” a
gruppi di 8 (appunto per formare i byte) ed ogni gruppo è dotato di un proprio
indirizzo:

Memoria a byte

Per accedere ad un qualsiasi byte di questa memoria, basta specificarne


l’indirizzo.
Al giorno d’oggi, la maggior parte delle memorie sono organizzate a parole da 32
bit:

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


18
Progetto dell’insieme di istruzioni

In questo caso, i byte continuano a possedere ciascuno un proprio indirizzo, ma


sussistono dei vincoli precisi per accedere alle varie locazioni di memoria:

• l'accesso ad un byte si può fare a un indirizzo qualsiasi: ad esempio 0, 1, 2,


3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F e così via ( 8);
• l'accesso ad una parola da 32 bit (4 byte) si deve fare a un indirizzo che sia
multiplo di 4: esempio 0, 4, 8, C e così via;
• l'accesso ad una parola da 16 bit (2 byte) si deve fare a un indirizzo che sia
multiplo di 2: ad esempio 0, 2, 4, 6, 8, A, C, E e così via.

Possiamo allora fare un discorso del tutto generale. In alcune macchine, l’accesso
ad oggetti più grandi di un byte (ad esempio le parole) richiede necessariamente il
cosiddetto allineamento. Consideriamo perciò un oggetto di S byte collocato
all’indirizzo A: si dice che l’accesso a tale oggetto è allineato se è verificata la
condizione
A mod S = 0

ossia sostanzialmente se A è un multiplo intero di S. In caso contrario, si parla di


accesso disallineato e talune macchine lo impediscono.
E’ evidente che per il valore di S ci sono almeno 4 possibilità:

• oggetto indirizzato: byte → S = 1


• oggetto indirizzato: parola corta → S = 2
• oggetto indirizzato: parola → S = 4
• oggetto indirizzato: parola doppia → S = 8

Allora sono evidenti le seguenti considerazioni:

• quando l’oggetto indirizzato è un byte (S=1), non può mai verificarsi il


disallineamento, in quanto A mod S = 0 sempre;
• quando l’oggetto indirizzato è una parola corta (S=2), il disallineamento o
l’allineamento si verificano a seconda dell’indirizzo della parola: ad esempio,
se A=0 oppure A=2 o A=4, si ottiene sempre A mod S = 0 (allineamento),
mentre invece, se A è un numero dispari (1,3,5,…), c’è sempre
disallineamento;

8
Si è usata una notazione esadecimale per esprimere gli indirizzi

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


19
Appunti di “Calcolatori elettronici” – Capitolo 3

• analogo discorso vale per le parole (S=4 → l’allineamento si ha solo se A è 0


oppure un multiplo di 4) e le parole doppie (S=8→ l’allineamento si ha solo
se A è 0 o un multiplo di 8).

La tabella seguente riassume i vari casi:

Indirizzo…

Oggetto indirizzato Allineato rispetto allo Disallineato rispetto allo


spiazzamento del byte spiazzamento del byte
Byte 0,1,2,3,4,5,6,7 (mai)
Parola corta 0,2,4,6 1,3,5,7
Parola 0,4 1,2,3,5,6,7
Parola doppia 0 1,2,3,4,5,6,7

Accessi allineati e disallineati ad oggetti. Gli “spiazzamenti dei byte” sono specificati per i tre
bit meno significativi dell’indirizzo

La figura seguente propone due evidenti casi di disallineamento in una memoria


organizzata a parole:

E’ evidente che l'accesso alla parola da 32 bit di indirizzo 6 non è allineato, in


quanto la parola presenta i due byte superiori nella parola di indirizzo 4 e i due byte
inferiori nella parola di indirizzo 8. Analogamente, anche la parola da 16 bit di
indirizzo D è disallineata, in quanto si trova al centro della parola di indirizzo C.
Si intuisce che i disallineamenti comportano complicazioni strutturali quando la
memoria è organizzata a parole, per cui vanno evitati. Possiamo infatti facilmente
renderci conto che un riferimento disallineato alla memoria comporta un numero
più o meno grande di altri riferimenti disallineati alla memoria . Ad esempio, nella
prossima figura è mostrato quello che accade quando si accede in modo disallineato
ad una parola in memoria in un sistema dotato di bus a 32 bit, supponendo che la
memoria stessa sia organizzata a parole:

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


20
Progetto dell’insieme di istruzioni

16 bit
Riferimento alla parola disallineato

144424443 144424443

32 bit 32 bit

32 bit

32 bit

CPU

Accesso ad una parola disallineato (di 16 bit): l’accesso è disallineato in quanto i 32 bit di
interesse non si trovano esattamente in corrispondenza di una parola indirizzabile, ma spostati
di 16 bit verso destra

I 32 bit di interesse sono dunque divisi tra la parte inferiore di una parola e la
parte superiore della parola successiva. Di conseguenza, la CPU o il sistema della
memoria deve effettuare due distinti accessi alla memoria (uno per ciascuna delle
due parole coinvolte) per acquisire la mezza parola superiore e la mezza parola
inferiore; le due metà vengono poi fuse per ottenere la parola intera. Con linguaggio
RTL (Register Transfer Language), le operazioni da compiere sono esprimibili nel
modo seguente:

MEM[ind] → A
MEM[ind+4] → B
A<<16 → A
B>>16 → B
A OR B → A

Le prime due istruzioni caricano le due parole di memoria (ad indirizzi ind e
ind+4) in due dispositivi di memoria (in particolare due latch), denominati A e B, che
si suppone usati per alimentare i due ingressi della ALU del processore ( 9); la terza
istruzione esegue uno shift a sinistra del valore contenuto in A ( 10), in modo che i 16
bit meno significativi prendano il posto di quelli più significativi e gli altri siano posti
a 0; la successiva istruzione compie invece uno shift a destra di 16 bit del valore
contenuto in B, in modo che, questa volta, i bit più significativi prendano il posto di
quelli meno significativi; l’ultima istruzione esegue l’OR di A e di B e pone il risultato
in A, in modo da ottenere finalmente la parola desiderata.
Questo semplice esempio mostra in modo evidente il motivo per cui gli accessi
disallineati andrebbero sempre evitati: mentre un accesso allineato ad una parola
richiede un’unica istruzione (del tipo MEM[ind]→A), un accesso non allineato
richiede una sequenza di 5 istruzioni del tipo appena descritto (e che l’altro è solo
una semplificazione di un qualcosa di più complesso), con una perdita di tempo
9
Per rappresentare il caricamento, si è usato l’operatore “→→ ”: se privo di ulteriori indicazioni, esso indica che il contenuto
della sorgente (posta a sinistra della freccia) viene integralmente copiato nella destinazione (a destra della freccia). Per
indicare, invece, una locazione di memoria, si è usata la notazione MEM[x], dove x è l’indirizzo della suddetta locazione.
Queste ed altre notazioni che in seguito approfondiremo fanno parte del cosiddetto linguaggio di descrizione
dell’hardware e sono derivate direttamente dal linguaggio C.
10
Ricordiamo che uno shift a sinistra di N bit di un numero binario equivale a moltiplicare tale numero per 2N.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


21
Appunti di “Calcolatori elettronici” – Capitolo 3

decisamente maggiore. Evidentemente, se la memoria fosse organizzata con moduli


indipendenti larghi ciascuno un byte anziché una parola, sarebbe invece possibile
accedere ai soli dati necessari e il disallineamento non si presenterebbe mai.
Una osservazione interessante è la seguente: come capire che un indirizzo
generato dalla CPU è disallineato? Possiamo rispondere tramite un semplice esempio:
consideriamo una memoria organizzata a parole, come nei precedenti esempi, e
supponiamo che gli indirizzi di memoria siano a 32 bit (per cui lo spazio di
indirizzamento è di 2 32 byte); nel caso si voglia accedere ad una parola (S=4),
abbiamo visto che un accesso allineato si ha quando A mod 4 = 0, dove A è l’indirizzo
di memoria prodotto dalla CPU; in termini pratici, quindi, A deve essere 0 o un
multiplo di 4; esprimendo allora A in binario, la suddetta condizione equivale a dire
che il valore binario di A deve avere gli ultimi due bit a 0:

xxxxxxxx xxxxxxxx xxxxxxxx xxxxxx00

Se l’hardware per l’accesso alla memoria rileva che almeno uno degli ultimi due bit
non è a 0, deduce che si tratta di un accesso disallineato e prende i necessari
provvedimenti. Ad esempio, per realizzare la sequenza di istruzioni riportata poco fa,
a partire dall’indirizzo A, bisogna generare due indirizzi, corrispondenti alle due
parole consecutive di memoria che contengono, al loro interno, la parola richiesta;
tali due indirizzi si ottengono nel modo seguente:

ind1 = A & (1) 30 ##(0) 2


ind2 = ind1 + 4

La prima istruzione fa semplicemente l’AND logico (&) dell’indirizzo A con una


maschera di 32 bit dove tutti i bit sono ad 1 tranne gli ultimi due, che sono a 0: in
questo modo, gli ultimi due bit di A vengono forzati a 0, in modo da ottenere
l’indirizzo della prima parola di memoria da prelevare. Il successivo indirizzo è invece
banalmente ottenuto incrementando il primo indirizzo di 4 byte. In questo modo, i
due indirizzi generati sono allineati e spetta poi ancora all’hardware “ricostruire” la
parola richiesta dalla CPU, secondo il meccanismo descritto poco fa.
Tutte queste considerazioni mostrano dunque che i programmi che rispettano gli
allineamenti vengono eseguiti più velocemente, anche su macchine che ammettono
accessi non allineati.
L'accesso non allineato alla memoria viene comunque eseguito a patto che la
memoria sia dotata di un'apposita unità funzionale di allineamento, detta appunto
dispositivo di allineamento; in generale, comunque, l'accesso non allineato richiede
più tempo dell'accesso allineato, ovvero richiede più cicli di clock.
Alcuni sistemi di memoria proibiscono l'accesso non allineato; altri lo consentono,
per ragioni di compatibilità con programmi scritti per processori con memoria avente
parole di dimensione inferiore; altri infine sono programmabili in entrambe le
modalità.

Dispositivo di allineamento
Anche se i dati sono allineati, l’esecuzione di un accesso a byte oppure a parole
corte richiede un dispositivo che allinei i byte o le parole corte nei registri. Non solo,
ma può anche capitare che la macchina debba estendere in segno il dato, a seconda
del tipo di istruzione che lo ha richiamato.

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


22
Progetto dell’insieme di istruzioni

In alcune macchine, il caricamento di un byte oppure di una parola corta non


modifica la parte più significativa dei registri interessati; dualmente, la scrittura in
memoria coinvolge solo la parte utilizzata dei registri.
La figura seguente mostra il dispositivo di allineamento usato per il caricamento
e la memorizzazione di un byte tra una parola di memoria ed un registro:

Byte nel registro


0 1 2 3

Dispositivo
di allineamento

0 1 2 3

Parola in memoria
Schematizzazione del dispositivo di allineamento per il caricamento e la memorizzazione di un
byte (si suppone che la parola di memoria sia a 32 bit)

Si considerano evidentemente parole di memoria a 32 bit. Per ogni parola sono


richiesti 4 bus di allineamento, uno per ogni byte della parola. L’accesso allineato a
parole corte richiederebbe due bus aggiuntivi per caricare, in modo esclusivo, il byte
0 o il byte 2 dalla memoria nel byte 2 del registro. Una memoria a 64 bit
richiederebbe un numero doppio di bus di allineamento per i byte e le parole corte ed
anche due bus di allineamento a 32 bit per l’accesso alle parole.
Il dispositivo di allineamento si limita a posizionare i byte durante l’operazione.
Per garantire che solo i byte delle celle di memoria corrispondenti a quelli usati
vengano modificati durante la scrittura della memoria, si usano segnali di controllo
aggiuntivi.
Invece di un dispositivo di allineamento, alcune macchine usano un registro a
scorrimento, per spostare i dati solo nei casi in cui è richiesto l’allineamento. Questo
procedimento rende molto lento l’accesso ad oggetti che non siano lunghi una parola,
ma elimina il dispositivo di allineamento ed incrementa perciò la velocità del caso
più frequente, che è appunto quello di accesso ad una parola.
Mentre tutte le macchine discusse in questo capitolo e nel prossimo consentono
l’accesso in memoria a byte ed a parole corte, nella realtà solo il calcolatore VAX ed il
microprocessore Intel 8086 permettono operazioni aritmetico-logiche tra operandi,
memorizzati nei registri, di dimensione inferiore alla parola.

M
Mood
daalliittàà d
dii iin
nddiirriizzzzaam
meen
nttoo
Dato un certo indirizzo, abbiamo esaminato nel paragrafo precedente le varie
possibilità di accesso ai byte in memoria. Adesso ci occupiamo invece delle
cosiddette modalità di indirizzamento, ossia dei modi in cui le architetture
specificano l’indirizzo degli oggetti a cui accedere.
Nelle macchine con registri di uso generale (cosiddette macchine GPR, General
Purpose Registers), la modalità di indirizzamento può specificare tre cose distinte:
una costante, un registro oppure una locazione di memoria. Nel caso specifico di un

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


23
Appunti di “Calcolatori elettronici” – Capitolo 3

indirizzamento a memoria, l’indirizzo fisico della cella di memoria interessata viene


detto indirizzo effettivo.
Nella prossima tabella vengono mostrate tutte le modalità di indirizzamento ai dati
che interessano le macchine discusse in questo capitolo, corredando ciascuna
modalità con un esempio di istruzione di somma (add) tra due operandi:

Modalità di Istruzione di esempio Significato Quando è usata


indirizzamento
Registro Add R4,R3 R4←R4+R3 Quando il valore è in un registro
Per costanti. In alcune macchine,
Immediato (o Add R4,#3 R4←R4+3 “immediato” e “letterale” sono due
letterale) diversi modi di indirizzamento
Spiazzamento (o Add R4,100(R1) R4←R4+M[100+R1] Accessi a variabili locali
con base)
A registro differito Add R4,(R1)
Accessi mediante puntatore o
R4←R4+M[R1]
(o indiretto) indirizzo calcolato
Usato talvolta per indirizzare una
Indicizzato Add R3,(R1+R2) R3←R3+M[R1+R2] matrice, nel qual caso R1 è la base
dell’array mentre R2 è l’indice
Utile talvolta per accedere a dati
Diretto (o Add R1,(1001) statici: può succedere che la
R1←R1+M[1000]
assoluto) costante di indirizzamento sia
grande
Se R3 è l’indirizzo per un puntatore
Memoria indiretta Add R1,@(R3) R1←R1+M[M[R3]]
p, allora il metodo fornisce *p
Utile per scandire array all’interno
di un ciclo: R2 punta all’inizio
R1←R1+M[R2] dell’array, ogni riferimento
Autoincremento Add R1,(R2)+
R2←R2+d incrementa R2 di una quantità
uguale alla dimensione d
dell’elemento
Si può usare in maniera analoga
all’autoincremento. In ogni caso,
R2←R2-d l’autoincremento e
Autodecremento Add R1,-(R2)
l’autodecremento possono essere
R1←R1+M[R2]
usati per la gestione di una pila
(stack)
Usato anche questo per indicizzare
gli array. Può essere applicato, in
Scalato Add R1,100(R2)[R3] R1←R1+M[100+R2+R3×d]
alcune macchine, ad ogni modalità
base di indirizzamento

Precisiamo che i nomi qui riportati sono quelli più comuni, ma comunque
differiscono da architettura ad architettura.
Per quanto riguarda il metodo con cui abbiamo descritto le varie modalità,
abbiamo fatto una scelta che ritroveremo spesso nei prossimi discorsi: abbiamo cioè
usato, come linguaggio di descrizione dell’hardware, un “ampliamento” del
linguaggio di programmazione C. Tale ampliamento ha comportato, per esempio,
l’introduzione di due “notazioni” non appartenenti al C: la freccia orientata a sinistra
(←) indica l’assegnamento di un valore ad un registro o ad una cella di memoria,
mentre invece l’identificatore “M” indica la memoria. La notazione M[R1] si riferisce
ad esempio alla locazione di memoria il cui indirizzo è contenuto nel registro R1.
L’ultimo paragrafo di questo capitolo è dedicato interamente alla descrizione del
linguaggio di descrizione dell’hardware.
Premesso questo, descriviamo le righe della tabella una per una:

• la prima riga fa riferimento ad istruzioni che indirizzano solo registri; viene


riportato un esempio di istruzione che fa riferimento a due registri: essi
contengono inizialmente i due operandi da sommare, mentre invece il primo
viene usato per memorizzare il risultato della somma;

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


24
Progetto dell’insieme di istruzioni

• la seconda riga corrisponde al cosiddetto indirizzamento immediato (o


letterale), in cui cioè uno o entrambi gli operandi sono specificati direttamente
nell’istruzione: nell’esempio riportato, il secondo operando è quello immediato,
mentre l’altro è contenuto in un registro, che a sua volta sarà usato per
contenere il risultato della somma ( 11);
• la terza riga corrisponde al cosiddetto indirizzamento con spiazzamento: uno
dei due operandi si trova in memoria (mentre l’altro è ancora una volta in un
registro) e il suo indirizzo deve essere ottenuto come somma di uno
spiazzamento (100 nell’esempio) e del contenuto di un altro registro (R1
nell’esempio);
• la quarta riga (indirizzamento indiretto) è simile alla precedente, con la
differenza che non è presente alcuno spiazzamento: l’indirizzo dell’operando in
memoria è contenuto nel registro specificato nell’istruzione (R1 nell’esempio);
• sempre sulla falsa riga delle due righe precedenti, la quinta riga riporta il
cosiddetto indirizzamento indicizzato, in cui l’indirizzo dell’operando in
memoria si ottiene come somma del contenuto di due registri, entrambi indicati
nell’istruzione. Si tratta di un metodo di indirizzamento particolarmente utile
per la gestione dei vettori (o, in generale, delle matrici): in questo caso, infatti,
uno dei due registri (R1) punta al primo elemento del vettore, mentre l’altro
(R2) esprime l’indice del vettore, in modo che la somma puntati ad un preciso
elemento del vettore stesso;
• la sesta riga riporta invece il classico caso di indirizzamento diretto, in cui
l’operando in memoria viene specificato direttamente tramite il suo indirizzo;
• la settima riga corrisponde al cosiddetto indirizzamento indiretto a due
livelli: viene in questo caso specificato un registro (R3) che contiene l’indirizzo
di una locazione di memoria (M[R3]) che a sua volta contiene l’indirizzo
dell’operando;
• l’ottava riga riporta il cosiddetto indirizzamento ad autoincremento, che è un
perfezionamento dell’indirizzamento indiretto: infatti, non solo l’indirizzo
dell’operando in memoria è contenuto in un registro (R2 nell’esempio), ma il
contenuto di tale registro viene anche aggiornato (dopo ovviamente il
prelevamento dell’operando) di una quantità pari alla dimensione dell’operando
prelevato. In questo modo, diventa facile indirizzare un array, come descritto
nella tabella stessa;
• la nona riga contiene la modalità duale della precedente, ossia il cosiddetto
indirizzamento ad autodecremento: il registro contenente l’indirizzo
dell’operando di memoria viene prima decrementato e poi usato per accedere
all’operando;
• infine, l’ultima riga riporta il cosiddetto indirizzamento scalato: l’indirizzo
dell’operando in memoria si ottiene sommando una costante (indicata
nell’istruzione) sia al contenuto di un primo registro (R2 nell’esempio) sia al
contenuto di un secondo registro (R3) moltiplicato per una costante pari alla
dimensione dell’operando stesso.

E’ importante rendersi conto che le modalità di indirizzamento possono ridurre


sensibilmente il numero di istruzioni da impiegare, ma aumentano la

11
Facciamo notare che questo tipo di indirizzamento viene di solito considerato come un indirizzamento a
memoria, anche se si accede solo a valori che fanno parte del codice eseguibile del programma stesso.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


25
Appunti di “Calcolatori elettronici” – Capitolo 3

complessità di costruzione della macchina . Di conseguenza, l’uso di varie modalità


di indirizzamento è molto importante per permettere al progettista di scegliere quelle
da introdurre.

FFrreeqquueen nzzee rreellaattiivvee d


dii u
uttiilliizzzzoo d
deellllee vvaarriiee m
mood
daalliittàà d
dii
iin
nddiirriizzzzaam
meen nttoo
Mentre i risultati di molte analisi sulle modalità di indirizzamento impiegate
dipendono dalla macchina utilizzata, altre sono ampiamente indipendenti
dall’architettura del calcolatore e noi ci concentreremo specialmente su questa
seconda categoria. Tuttavia, prima di esaminarle, riteniamo opportuno esaminare le
frequenze relative di utilizzo delle varie modalità di indirizzamento, ossia la frequenza
con cui, in un dato programma, si presentano le varie modalità.
Nella prossima figura vengono illustrati i risultati del calcolo della frequenza
relativa delle modalità di indirizzamento in alcuni programmi di benchmark sul
calcolatore VAX, che dispone di tutte le modalità di indirizzamento descritte nel
paragrafo precedente. I programmi usati sono i seguenti:

• Gnu C Compiler (GCC);


• Spice;
• TeX.

I risultati sono i seguenti:

Memoria TeX
Spice
indiretta GCC
TeX
Scalato Spice
GCC
TeX
A registro Spice
indiretto GCC
TeX
Spice
Immediato GCC
TeX
Con spiazzamento Spice
GCC

10% 20% 30% 40% 50% 60%

Sono qui riportate solo le modalità di indirizzamento che hanno mostrato una
frequenza relativa d’uso superiore all’ 1% ( 12).
Consideriamo ad esempio l’indirizzamento con spiazzamento, riportato in basso: il
programma che ne fa più uso è Spice, per il quale la frequenza relativa risulta essere
del 55%, il che significa che più di un una istruzione su due (nell’ambito ovviamente
di quelle con operandi) contiene questo tipo di modalità di indirizzamento. Come si

12
Non sono state nemmeno considerate le cosiddette modalità di indirizzamento relative al PC: sono così
definite quelle modalità di indirizzamento che dipendono dal registro contatore di programma (PC, Program
Counter), le quali vengono usate principalmente per il controllo del flusso di esecuzione nelle istruzioni di
salto.

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


26
Progetto dell’insieme di istruzioni

vede, questo 55% è il valore più alto riportato nel grafico; il valore più basso si
ottiene per l’indirizzamento scalato in TeX: la frequenza relativa è molto prossima
allo 0%.
Possiamo corredare il grafico con le seguenti considerazioni:

• per quanto riguarda l’indirizzamento con spiazzamento, è opportuno


sottolineare che lo spiazzamento comprende tutte le lunghezze ammissibili
(vale a dire 8, 16 e 32 bit);
• non sono state evidenziate le modalità di tipo registro, che comprendono
circa la metà dei riferimenti ad operandi, mentre invece gli indirizzamenti a
memoria (compreso quello immediato) comprendono l’altra metà;
• sul calcolatore VAX preso in esame, la modalità di indirizzamento indiretto a
memoria può essere dotata di uno spiazzamento, di un autoincremento o di
un autodecremento a partire dall’indirizzo iniziale di memoria;
• ricordiamo che il compilatore influisce sulle frequenze relative d’uso delle
modalità di indirizzamento, come diremo in seguito;
• concludiamo osservando che le modalità di indirizzamento principale
comprendono tutte le operazioni di accesso a memoria, tranne una piccola
frazione (dallo 0% al 3%).

M daalliittàà d
Mood nd
dii iin meen
diirriizzzzaam piiaazzzzaam
n ssp
nttoo ccoon nttoo
meen
Nella modalità di indirizzamento con spiazzamento, il problema principale da
considerare riguarda l’estensione dello spiazzamento stesso, che ovviamente
condiziona in modo pesante la lunghezza delle istruzioni che usano questa modalità.
E’ opportuno allora studiare proprio la frequenza relativa d’uso di spiazzamenti di
varia estensione, individuando quello più frequente e dimensionando il massimo
spiazzamento di conseguenza.
Dai tipici grafici sulla frequenza relativa d’uso di spiazzamenti di varia estensione
si deduce che gli spiazzamenti piccoli sono quelli più frequenti, ma ci sono
comunque anche una certa quantità di valori grandi.
Segnaliamo inoltre che lo spiazzamento ha sia un valore sia una direzione (o
segno): quest’ultimo dipende fortemente dall’allocazione della memoria; la maggior
parte degli spiazzamenti sono positivi, ma la maggior parte degli spiazzamenti grandi
sono negativi. Anche in questo caso, molto dipende dalla politica di allocazione
seguita dal compilatore e quindi capita che la situazione vari a seconda del
compilatore impiegato.

M
Mood
daalliittàà d
dii iin
nddiirriizzzzaam
meen
nttoo iim
mmmeed
diiaattoo
Gli operandi immediati vengono usati nelle operazioni aritmetiche, nei confronti
(soprattutto in funzione delle diramazioni) e nei caricamenti finalizzati ad
inizializzare i registri tramite delle costanti. Quest’ultimo caso è tipicamente legato
sia alla presenza di costanti incluse nel codice eseguibile (che comunque sono in
numero modesto) sia all’uso di indirizzi prefissati (caso frequente).
Una delle maggiori problematiche da affrontare, per l’uso di operandi immediati, è
la necessità di verificare se occorre che tali operandi siano disponibili per tutte le
operazioni oppure per solo una parte di esse. Anche in questo caso, le stime vengono

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


27
Appunti di “Calcolatori elettronici” – Capitolo 3

condotte tramite analisi di frequenze relative analoghe a quelle già viste nei
precedenti paragrafi.
Inoltre, è importante anche caratterizzare l’intervallo di esistenza dei valori
immediati, in quanto, così come per gli spiazzamenti, tale intervallo influisce sulla
dimensione delle istruzioni. Ad esempio, nel caso di operandi corrispondenti a
costanti incluse nel codice eseguibile, si verifica che generalmente tali costanti sono
molto piccole, mentre invece, nel caso di indirizzi prefissati, i valori sono spesso
grandi. Una costante di particolare importanza è lo zero: si tratta di una costante
che, come diremo anche in seguito, è molto usata negli eseguibili (si pensi
tipicamente alle istruzioni di confronto con 0), tanto che, in calcolatori con il VAX,
sono state previste apposite istruzioni per i confronti con lo 0 e/o l’inizializzazione a
0 di parole di memoria: questo al fine di ridurre il numero di istruzioni che fanno uso
di un operando immediato pari a 0, come infatti rivelano appositi grafici.

C
Cood
diiffiiccaa d
deellllee m
mood
daalliittàà d
dii iin
nddiirriizzzzaam
meen
nttoo
La tecnica con cui le varie modalità di indirizzamento vengono codificate dipende
strettamente sia dalla loro varietà sia dal loro grado di indipendenza dai codici
operativi delle istruzioni:

• un numero abbastanza limitato di modalità di indirizzamento si può


codificare direttamente nel codice operativo, aggiungendovi un numero
opportuno di bit oltre quelli che descrivono il tipo di istruzione e le azioni da
compiere. Questa tecnica è usata ad esempio nel calcolatore IBM 360, il
quale dispone di solo cinque modalità di indirizzamento e in cui la maggior
parte delle operazioni ammettono solo uno o due indirizzamenti distinti;
• se invece le combinazioni sono in numero maggiore, allora è solitamente
necessario usare, per ciascun operando, un apposito campo indicatore di
indirizzamento: il valore di tale campo determina la modalità di
indirizzamento usata per l’operando; la lunghezza di tale campo dipende da
quante modalità sono ammesse: per N modalità, serviranno almeno log 2 N
bit.

Come già abbiamo osservato in precedenza, il numero di registri ed il numero di


modalità di indirizzamento ammesse condizionano fortemente la dimensione delle
istruzioni: questo perché il campo per la modalità di indirizzamento e quello per i
registri possono comparire anche più di una volta nella stessa istruzione, tanto che
la maggior parte delle istruzioni impiegano molti più bit per la codifica degli
indirizzamenti e dei registri che non per l’espressione del codice operativo stesso. Il
progettista dell’insieme di istruzioni deve allora trovare un compromesso tra almeno
3 esigenze contrastanti:

• in primo luogo, è sempre opportuno poter disporre di quanti più registri


possibile e di quante più modalità di indirizzamento possibile, il che
richiederebbe un numero di bit elevato per la codifica;
• del resto, quanti più bit sono necessari per la codifica di registri e modalità
di indirizzamento, tanto più lunghe (mediamente) diventano le istruzioni e
quindi anche i programmi nel loro complesso;
• in generale, è anche opportuno ottenere codifiche delle istruzioni che
risultino poi facili da gestire nella realizzazione concreta delle macchine. Ad

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


28
Progetto dell’insieme di istruzioni

esempio, un requisito più che opportuno è che le istruzioni abbiano sempre


una lunghezza multipla di un byte e non arbitraria. Una scelta che è stata
fatta su alcune macchine è quella di definire istruzioni di dimensione
prefissata al fine di ottenere vantaggi realizzativi, ma questo va spesso a
scapito della lunghezza media dei programmi, perché si finisce per usare, in
un numero più o meno grandi di casi, istruzioni più lunghe di quanto
sarebbe in effetti necessario.

Per concludere, osserviamo che la tecnica di codifica impiegata condiziona


pesantemente la facilità di decodifica delle istruzioni da parte della macchina,
proprio perché la maggioranza dei bit di ciascuna istruzione sono dovute alla codifica
degli indirizzamenti e non a quella del codice operativo.

Operandi dell’insieme di istruzioni


Gli operatori che sono supportati dalla maggior parte delle architetture degli
insiemi di istruzioni possono essere classificati come nella seguente tabella:

Tipo di operatore Esempi


Aritmetico e logico Operazioni aritmetiche e logiche: somma, AND,
sottrazione, OR.
Trasferimento di dati Load/Store (istruzioni di trasferimento per macchine con
indirizzamento in memoria)
Controllo Diramazione, salto, chiamata di procedura, ritorno da
procedura, trappole
Sistema Chiamate del sistema operativo, istruzioni di gestione
della memoria virtuale
Virgola mobile Operazioni in virgola mobile: somma e moltiplicazione
Decimale Somma decimale, moltiplicazione decimale, conversione
da carattere a decimale
Stringa Spostamento di stringhe, confronto tra stringhe, ricerca
su una stringa

Tutte le macchine forniscono generalmente un insieme completo di operazioni per


le prime tre categorie: operazioni aritmetiche e logiche, operazioni di
trasferimento di dati ed operazioni di controllo.
Per quanto riguarda, invece, le funzioni di sistema disponibili, esse variano
fortemente tra le architetture, anche se comunque tutte le macchine forniscono
quelle basilari.
Per quanto riguarda, infine, le ultime tre categorie (operazioni in virgola mobile,
operazioni in decimale, operazioni su stringhe), le funzionalità supportate variano
da nessuna ad un insieme più o meno esteso di funzioni speciali. In particolare, le
istruzioni in virgola mobile vengono fornite su ogni macchina che sia specificamente
rivolta ad una applicazione che ne faccia uso. Le istruzioni decimali e le istruzioni per
le stringhe sono talvolta preesistenti, come avviene nel VAX o nell’IBM 360, mentre
talvolta vengono sintetizzate dal compilatore con semplici istruzioni.
Ci occuperemo in seguito, in modo generale, dell’uso delle varie operazioni, come
ad esempio i riferimenti a memoria, le operazioni ALU e le diramazioni condizionate.
Vedremo anche l’uso delle varie istruzioni per alcune differenti architetture. Ci

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


29
Appunti di “Calcolatori elettronici” – Capitolo 3

occupiamo invece adesso specificamente solo delle istruzioni di controllo, per due
motivi:

• in primo luogo, queste istruzioni sono fortemente indipendenti dalle altre


scelte relative all’insieme di istruzioni;
• in secondo luogo, la “misura” del comportamento dei salti e delle
diramazione condizionate è ragionevolmente indipendente dalle altre misure.

IIssttrru
uzziioon
nii p
peerr iill ccoon
nttrroolllloo d
deell ffllu
ussssoo
Cominciamo col dire che non esiste una terminologia uniforme per le istruzioni
che possono modificare il flusso di controllo di un programma:

• fino all’ IBM 7030 (anno 1960), le istruzioni di controllo del flusso erano
chiamate tipicamente istruzioni di trasferimento (transfer); l’introduzione
del 7030 comportò l’uso della parola diramazione (branch), che fu usata per
indicare indistintamente tutte le istruzioni di trasferimento del controllo;
• successivamente, le nuove macchine introdussero anche nuove
denominazioni: nei prossimi discorsi noi useremo il termine salto
(jump) quando il controllo è incondizionato e il termine diramazione
quando invece esso è soggetto a condizione .

In generale, possiamo individuare quattro diversi tipi di controllo del flusso:

• diramazioni (condizionate);
• salti (incondizionati);
• chiamate a procedura;
• ritorni da procedura.

Il nostro scopo è quello di conoscere la frequenza relativa con cui si presentano


tali “eventi”: dato che si tratta di eventi diversi, è possibile che siano impiegate
istruzioni diverse per ognuno e che anche i successivi “comportamenti” siano
differenti. Se prendiamo in esame una macchina load/store (cioè in cui tutte le
istruzioni ALU hanno operandi solo nei registri) e misuriamo la frequenza relativa
delle istruzioni di controllo del flusso nel caso dei soliti programmi TeX, Spice e GCC,
otteniamo i seguenti risultati:

• le diramazioni sono gli eventi decisamente più frequenti (66% per TeX, 75%
per Spice e 78% per GCC);
• le chiamate a procedura ed i ritorni da procedura si attestano su valori
compresi tra 10% (GCC) e 16% (TeX);
• i salti hanno frequenze, rispettivamente, di 18% per TeX e 12% sia per Spice
sia per GCC.

Nel caso delle diramazioni, è importante specificare l’indirizzo di destinazione cui


bisogna giungere se la condizione viene verificata: tale destinazione è specificata
quasi sempre in modo esplicito, ma comunque non sempre; ad esempio, nel ritorno
da procedura essa non viene specificata, in quanto l’indirizzo di ritorno è noto sin

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


30
Progetto dell’insieme di istruzioni

dall’istante della compilazione: si tratta infatti dell’indirizzo dell’istruzione successiva


a quella che ha chiamato la procedura e quindi è noto a priori.
Il modo più classico per specificare la destinazione è quella di fornire uno
spiazzamento da sommare al registro contatore di programma (PC, Program Counter):
in questo caso, si parla di diramazioni relative al program counter. Si tratta di
casi vantaggiosi in quanto la destinazione della diramazione è spesso vicina
all’istruzione corrente e quindi il fatto di specificare la posizione relativamente al
program counter consente di usare pochi bit. Non solo, ma un simile indirizzamento
consente di eseguire il codice in qualunque posizione venga caricato, conferendo al
codice la cosiddetta proprietà di indipendenza dalla posizione; questa proprietà
può eliminare una parte del lavoro del cosiddetto linker (collegatore) e risulta utile
anche nei programmi in cui il collegamento avviene durante l’esecuzione.
Quando la destinazione è sconosciuta al momento della compilazione, è richiesto
un ulteriore metodo di indirizzamento oltre quello relativo al program counter: deve
infatti esistere un modo per specificare in modo dinamico la destinazione, al fine
appunto di poterla modificare durante l’esecuzione (run-time). Un modo semplice di
risolvere il problema potrebbe essere quello di memorizzare l’indirizzo di destinazione
in un registro e fare riferimento ad esso. In alternativa, la diramazione può
consentire, per fornire l’indirizzo di destinazione, una qualsiasi delle modalità di
indirizzamento permesse dalla macchina.
Un’altra importante questione riguarda la “distanza” di una diramazione dalla sua
destinazione: infatti, la conoscenza di come si “distribuiscono” questi spiazzamento è
di aiuto per scegliere quali supportare e tale scelta a sua volta influenza sia la
lunghezza dell’istruzione sia la sua codifica.
Dato che, così come confermato dai risultati riportati prima, la maggior parte dei
cambiamenti nel flusso di controllo è da imputare alle diramazioni, risulta anche
importante il modo in cui specificare le condizioni associate a tali diramazioni.
Esistono in proposito tre distinte tecniche:

• tecnica dei codici condizioni (CC): in questo caso, dei “bit speciali” (detti
flag, cioè indicatori) vengono modificati dalle operazioni dell’ALU,
eventualmente sotto il controllo del programma. Ad esempio, possiamo avere
i seguenti flag: Z (risultato nullo), N (risultato negativo); V (overflow), C
(riporto), I (interrupt), D (decimale), F (floating).
Il vantaggio di questa tecnica è che, in alcuni casi particolari, la condizione
è valutata a costo zero; a fronte di ciò, però, il codice delle condizioni
vincola l’ordinamento delle istruzioni, dato che esse passano informazioni
alla diramazione;

• tecnica del registro condizioni: il risultato del confronto viene sottoposto


ad un test e il risultato del test viene a sua volta memorizzato in un registro,
che viene letto per decidere se eseguire la diramazione oppure no. Il
suddetto test si può ad esempio effettuare facendo la AND del risultato con
una maschera di bit, scelta in modo tale che l’esito del test sia univocamente
comprensibile alla macchina. Tanto per fare un esempio, supponiamo che la
diramazione sia basata sulla verifica se il contenuto del registro R1 sia
positivo oppure negativo; se, per semplicità, supponiamo che il registro R1
sia ad 8 bit, di cui quello più significativo rappresentativo del segno (1 per
negativo e 0 per positivo), possiamo fare la AND di R1 con la maschera
10000000 e porre il risultato nel registro R2; allora, il contenuto di R2
risulterà pari a 10000000 se il contenuto di R1 è negativo, mentre sarà
00000000 in caso contrario, per cui, leggendo il primo bit di R2, si capisce il
segno del valore contenuto in R1.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


31
Appunti di “Calcolatori elettronici” – Capitolo 3

L’uso del registro aggiuntivo (detto registro di programmazione) è un sicuro


svantaggio di questa tecnica, che però risulta anche la più semplice;
• tecnica del confronto e diramazione: questa tecnica non è altro che una
evoluzione della precedente, in quanto non richiede l’uso del registro di
programmazione e consente di effettuare il confronto direttamente: ad
esempio, si potrebbe prevedere una istruzione del tipo CMP R1,#80,α , dove
ovviamente CMP sta per “compare”, ossia “confronta”: l’istruzione richiede il
confronto del contenuto di R1 con il valore 80 (operando immediato) e,
implicitamente, dice che, se risulta R1=80, bisogna saltare all’istruzione
all’indirizzo α (altro operando immediato). Ovviamente, anziché verificare
l’uguaglianza tra il registro ed il valore immediato, si può verificare una
qualsiasi altra relazione d’ordine (ad esempio “minore di” oppure “maggiore
di” e così via).
Il vantaggio è dunque quello di usare una sola istruzione anziché due, ma
questo si paga con il rischio che ci sia troppo lavoro per istruzione.

L’importanza dello zero nei test di uguaglianza


Una delle proprietà più importanti delle diramazioni è che un gran numero di
confronti si riduce ad un semplice test di uguaglianza e, in più, molti di questi test
hanno con riferimento lo zero. In base a questa osservazione, alcune architetture
“vedono” questi confronti come casi speciali, specialmente quando viene usata una
istruzione del tipo “confronta ed esegui una diramazione” e prevedono perciò
istruzioni apposite per questi casi: ad esempio, si può pensare ad una istruzione di
diramazione del tipo CMP R1,α, in cui viene verificata la condizione R1=0 e, in caso
di uguaglianza, viene effettuato il salto all’istruzione di indirizzo α.
Apposite misure sulla frequenza dei diversi confronti usati per le diramazioni
mostrano il seguente risultato: una larga percentuale delle diramazioni ha un
operando immediato (86%) e lo 0 è l’operando immediato più frequente (83%).
Combinando questo risultato con quelli precedentemente descritti sulla frequenza
relativa delle istruzioni di modifica del flusso di controllo, si arriva a dedurre che
oltre il 50% dei confronti nelle diramazioni sono semplici test di uguaglianza a zero.

Ulteriori considerazioni
Una diramazione si dice eseguita se la condizione da essa verificata è vera e se
l’istruzione successiva da eseguire corrisponde all’obbiettivo della diramazione. Ad
esempio, si comprende immediatamente che tutte le istruzioni di salto (essendo
incondizionate) siano eseguite.
Come già osservato in precedenza, le diramazioni hanno anche una direzione, in
quanto la destinazione può trovarsi prima o dopo della diramazione che fa
riferimento ad essa. Molte diramazioni all’indietro sono cicli a condizione (un ciclo
while, un ciclo for e simili). Complessivamente, il comportamento delle diramazioni
dipende dall’applicazione e talvolta anche dal compilatore: quest’ultima dipendenza è
dovuta ai cambiamenti che il compilatore effettua sul flusso di controllo per
ottimizzare il tempo di esecuzione dei cicli.
Un altro aspetto importante da considerare riguarda specificamente le chiamate a
procedura e i successivi ritorni da procedura: infatti, per queste istruzioni è
necessario prevedere non solo un trasferimento del controllo all’inizio della
procedura chiamata, ma eventualmente anche una forma di salvataggio di stato; si

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


32
Progetto dell’insieme di istruzioni

deve infatti quantomeno salvare l’indirizzo di ritorno. Alcune architetture


forniscono allora un meccanismo implicito per il salvataggio dei registri, mentre
invece altre richiedono al compilatore di generare le opportune istruzioni.
Vi sono due meccanismi base tradizionalmente in uso per il salvataggio dei
registri:

• meccanismo caller-saving (salvato dal chiamante): significa che la


procedura chiamante deve salvare i registri se vuole conservarli per
eventuali accessi successivi alla chiamata;
• meccanismo called-saving (salvato dal chiamato): significa che la procedura
chiamata deve salvare i registri che vuole usare.

Un tipico caso di applicazione del primo metodo è quando si deve accedere a


variabili visibili globalmente in due diverse procedure; supponiamo ad esempio che
una procedura P1 chiami una procedura P2 e che entrambe manipolino la variabile
globale X: se P1 ha allocato X in un registro, deve assicurarsi di averlo salvato in
una locazione nota a P2 prima di chiamare P2 stessa. Per risolvere situazioni
complesse come e più di quella appena descritta, la maggior parte dei compilatori
provvede a far salvare, alla procedura chiamante, tutte le variabili a cui le procedure
chiamate potrebbero accedere.
In molti casi, è possibile usare sia il metodo caller-saving sia il metodo called-
saving e la scelta va fatta caso per caso, secondo criteri di convenienza. Per questo
motivo, i migliori compilatori usano una combinazione dei due meccanismi.

Tipo e dimensione degli operandi


Per “tipo” di un operando intendiamo semplicemente se si tratta di un intero, un
numero in virgola mobile (in singola o doppia precisione) o un carattere. Esistono
due alternative principali per decidere il tipo di operando contenuto in una
istruzione:

• nella prima, che poi è quella usata più di frequente, il tipo di operando viene
designato dal codice operativo, tramite un numero opportuno di bit;
• nella seconda, usata più raramente, i dati vengono segnati tramite
marcatori che vengono interpretati direttamente dall’hardware.

Normalmente, il tipo di un operando fornisce anche la sua dimensione. I tipi


comuni e le relative dimensioni sono i seguenti:

• carattere – 1 byte
• parola corta – 16 bit
• parola – 32 bit
• virgola mobile in singola precisione – 32 bit
• virgola mobile in doppia precisione – 64 bit

I caratteri possono essere rappresentati in due modi:

• le architetture dei mainframe IBM usano il formato EBCDIC;


• tutte le altre architetture usano il formato ASCII.

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


33
Appunti di “Calcolatori elettronici” – Capitolo 3

Gli interi sono invece ormai universalmente rappresentati tramite numeri


binari in complemento a due .
Per quanto riguarda, invece, i numeri in virgola mobile, fino a poco tempo fa ogni
costruttore aveva una propria rappresentazione, mentre invece adesso è ormai
diffusissimo l’uso dello standard IEEE 754.
Alcune architetture supportano, specialmente per le applicazioni finanziarie, un
formato decimale detto decimale impaccato (packed decimal): si tratta di un
formato decimale codificato in binario (BCD, binary-coded decimal), in base al quale
ogni cifra decimale (0,1,2,...,9) viene codificata con 4 bit, per cui un byte ospita due
cifre decimali. Le stringhe di caratteri numerici sono spesso dette decimali non
impaccati (unpacked decimal) e le operazioni su di esse, dette packing e unpacking,
sono abitualmente fornite per effettuare le conversioni bidirezionali tra i due tipi.
I benchmark che abbiamo scelto di usare per le nostre misure usano i seguenti
tipi di dati: caratteri da 1 byte, interi corti (o short integer) da 16 bit, interi da 32 bit
e numeri in virgola mobile. Studiando, come al solito, la distribuzione dinamica delle
dimensioni degli oggetti referenziati in memoria da questi programmi e la frequenza
di accesso ai diversi tipi di dati, si riesce a dedurre quali siano quelli da supportare
in modo più efficiente. Ad esempio, si vede che gli accessi ai dati che occupano
parole e parole doppie sono di gran lunga quelli dominanti e quindi sono quelli da
rendere più efficienti. Può avere senso chiedersi se sia meglio una macchina con un
accesso a memoria da 64 bit (che quindi impieghi un ciclo di clock per ogni parola
doppia) oppure una macchina con accesso a 32 bit (che impieghi 2 cicli di clock per
ogni parola doppia). Inoltre, ha senso anche chiedersi se, tra le primitive di accesso
alla memoria, abbia senso prevedere anche l’accesso ai singoli byte, cosa che
richiederebbe, come visto in precedenza, un dispositivo di allineamento.

Autore: Sandro Petrizzelli aggiornamento: 6 luglio 2001


34
Progetto dell’insieme di istruzioni

Linguaggio di descrizione dell’hardware

Notazione Significato Esempio Significato


Trasferimento di dati. La R1←R2 Trasferimento del contenuto
dimensione dei dati è data di R2 in R1. I registri hanno
dalla dimensione della dimensione prefissata, in
destinazione. Il loro numero modo che nel trasferimento di
← va specificato se non è dati da registri più piccoli
implicito verso registri più grandi sia
necessario specificare quali
bit trasferire
Vettore di memoria trasferito R1←M[x] Scrittura del contenuto della
per byte. L’indirizzo iniziale di locazione di memoria x in R1.
un trasferimento è quello Se il trasferimento inizia da
M iniziale del vettore M[i] ed è di 4 byte, i byte
trasferiti sono M[i], M[i+1],
M[i+2], M[i+3]
Trasferimento di un campo di M[y]← 16 M[x] Trasferimento di 16 bit,
n bit. Si usa ogni volta che la iniziando dalla locazione di
←n dimensione del trasferimento memoria x fino alla locazione
è determinata y. Le dimensioni dei due
blocchi devono coincidere.
Selezione del bit n di x R1 0 ←0 Modifica del bit più
significativo di R1 (segno) al
valore 0. Ricordiamo che I bit
xn sono numerati in ordine MSB
(Most Significant Bit),
iniziando con 0
Selezione di un campo di bit R3 24…31 ←M[x] Spostamento del contenuto
della locazione di memoria x
x m..n nel byte meno significativo di
R3
n
Replica di un campo R3 0…23 ←0 24 Inizializzazione dei tre byte
x più significativi (bit da 0 a 23)
di R3 al valore 0
Concatenamento di due R3←0 24 ## M[x] Spostamento del contenuto
campi della locazione x nel byte
meno significativo di R3.
##
Reinizializzazione dei tre
F2##F3← 64 M[x] byte più significativi.
Simbolo di puntatore; p* ← &x Assegnamento dell’indirizzo
*,& estrazione dell’indirizzo di della variabile x (&x)
una variabile all’oggetto puntato da p (p*)
Scorrimento logico (nel R1<<5 Scorrimento di R1 di 5
<<,>> linguaggio C) a sinistra o a posizioni
destra
Operatori condizionali (nel (R1==R2) & (R3!=R4) Espressione logica che
==, !=, >, <, linguaggio C): uguale, fornisce risultato true (vero)
diverso, maggiore, minore, se R! è uguale ad R2 e R3 è
>=, <= maggiore o uguale, minore o iverso da R4.
uguale
Operatori di bit (nel (R1 & (R2 | R3)) Prodotto logico, per bit, di R1
linguaggio C): prodotto logico con la somma logica (sempre
&, |, ^, ! (AND), somma logica (OR), per bit) di R2 ed R3
differenza simmetrica (XOR)
e complemento (NOT)

Autore: Sandro Petrizzelli


e-mail: sandry@iol.it
sito personale: http://users.iol.it/sandry

aggiornamento: 6 luglio 2001 Autore: Sandro Petrizzelli


35