Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
1
3.8.1 Progetto di un contatore sincrono modulo N con FF J-K 49
3.8.2 Progetto di un contatore sincrono modulo N con FF tipo D 50
3.9 Elementi di progettazione dei computer – stima dei costi 51
3.9.1 Il processo produttivo di un Circuito Integrato 51
6. Architetture Parallele 80
6.1 Il Pipelining 80
6.1.1 Valutazione delle prestazioni della pipeline (CPI) 83
6.1.2 Pipeline hazards (Conflitti potenziali nella pipeline) 86
6.2 Memoria e prestazioni dei Computer 89
6.2.1 Memoria Cache 90
6.2.1.1 Cache a mappatura diretta 92
6.2.1.2 Cache n-way set-associative 92
6.2.1.3 Sincronizzazione cache-memoria principale 93
6.2.2 Trattamento dei salti 94
6.2.2.1 Previsione dinamica dei salti 95
7. Architetture Superscalari 97
7.1 Evoluzione della scalabilità architetturale dei processori: 97
7.2 Unità Funzionali presenti nei processori superscalari 99
7.2.1 Decodifica parallela 99
2
7.2.2 Distribuzione superscalare 99
7.2.3 Esecuzione parallela 99
7.2.4 Gestione delle eccezioni 100
7.3 Limitazioni architetturali al parallelismo 100
7.3.1 Dipendenza vera dai dati 100
7.3.2 Dipendenza da flusso procedura (salto) 100
7.3.3 Conflitto di risorse 100
7.3.4 Politica di distribuzione (attacco) e ritiro (completamento) delle istruzioni 101
7.3.5 Altre politiche di distribuzione delle istruzioni 105
7.3.6 False dipendenze sui dati. 105
7.3.7 Dipendenze sui controlli. 105
7.3.8 Shelving (distribuzione indiretta). 106
7.3.9 Trattamento del blocco della distribuzione 107
7.3.10 Le politiche di distribuzione più usate e rispettivo trend 109
7.3.11 Il tasso di distribuzione 110
3
1. Approccio strutturato alle architetture dei calcolatori
1.1 Introduzione
"Un Elaboratore è una macchina in grado di risolvere problemi eseguendo istruzioni appositamente
2
specificate"
Applicando tale definizione a tutti i nomi che abbiamo introdotto precedentemente, si fa chiarezza e si
eliminano quelle “visioni” parziali e suggestive che ciascun diverso sinonimo intrinsecamente suscita.
In tale definizione di “Elaboratore” è opportuno soffermarsi sui seguenti elementi e sulle relative
interpretazioni:
Macchina intesa come “hardware” ovvero un insieme di circuiti elettronici digitali connessi in modo da poter
eseguire delle istruzioni.
Istruzioni intese come “software” ovvero un insieme ordinato di istruzioni specifiche che costituiscono un
programma. La specifica delle istruzioni è chiamato linguaggio di programmazione.
La definizione introduce implicitamente, due ulteriori termini: “hardware” e “software”, lasciando intendere
che l’elaboratore, se limitiamo la visione alla macchina di per se stessa ovvero al solo “hardware” non
sarebbe in grado di funzionare, riducendosi ad un’inutile “ferraglia”.
L’elaboratore diventa “operativo”, funzionante ed utile solo quando vengono preparate e predisposte le
opportune e corrette istruzioni da eseguire, ovvero quando viene inserito il relativo software.
Questo non significa, d’altra parte, che il software sia il principale elemento costituente un elaboratore.
Seguendo la metafora organicistica del famoso apologo di Menenio Agrippa, il software è un componente
funzionale, un organo indispensabile ma, da solo, senza la “Macchina” ovvero senza hardware, non
avrebbe alcuna utilità pratica.
In definitiva, non esiste una netta separazione tra hardware e software:
Hardware e software sono strettamente correlati in quanto ciascuno di essi da solo non è in grado di
svolgere alcuna funzione.
1
Qui e nel seguito del testo, il termine specializzazione deve essere inteso secondo quanto previsto dal linguaggio di modellazione (o
meta-linguaggio) UML: Relazione tassonomica tra un elemento più generale e uno più specifico. L'elemento più specifico è
pienamente consistente con quello più generale e contiene informazioni addizionali. Una istanza dell'elemento più specifico può
essere usata là dove e' consentito l'uso dell'elemento più generale.
2
Frase iniziale del libro “Structured Computer Organization” – Andrew S. Tanenbaum
4
Il linguaggio di programmazione, introdotto sopra come specifica delle istruzioni, è correlato sia all'hardware
che al software, in una sorta di dualità funzionale che ha, come target finale, l’uomo, ovvero chi dovrà trarre
vantaggio dall’utilizzo del computer.
L’uomo, questo “terzo incomodo”, che pretende di trarre vantaggio dall’utilizzo del computer, viene troppo
spesso trascurato, nonostante esso sia il fine ultimo, ma troppo spesso l’anello mancante, di tutta la ricerca
e sviluppo sugli elaboratori.
E’ quindi opportuno che, nella definizione di “Elaboratore” includiamo l’uomo, nascosto, nella definizione
stessa, dietro al “problema”. L’uomo diventa sia “committente” che “beneficiario” e, sempre riprendendo
l’apologo di Menenio Agrippa, fa esso stesso parte irrinunciabile di un sistema organico complessivo in cui
l’elaboratore stesso è uno strumento.
A questo punto appare più evidente la natura della dualità hardware-software: più il linguaggio è vicino alla
macchina e più sarà complesso per l’uomo programmarla; più il linguaggio è vicino all'utilizzatore (ovvero
all'uomo) e più sarà complesso per la macchina eseguire le corrispondenti istruzioni.
Per cercare di conciliare questa dualità, normalmente si separano i due processi precedenti ovvero si
definiscono due figure distinte (specializzazioni dell’actor “uomo”):
a) il programmatore, ovvero una sorta di “interprete linguistico”, in grado cioè di comprendere ed usare
un linguaggio vicino alla macchina ma distante dall’uomo e quindi riuscire a risolvere un problema
che ha l’utente, indicando alla macchina la giusta sequenza di istruzioni da compiere.
b) L’utente, ovvero l’uomo che deve risolvere il problema grazie all’ausilio dell’elaboratore ovvero che
sa cosa vuole ottenere ma non è in grado di esprimerlo con quel linguaggio “astruso” che
l’Elaboratore (e spesso anche il programmatore) conosce.
Grazie a questa separazione di ruoli, l’utente potrà usare l’elaboratore ed il programma come fossero un
tutt’uno indistinguibile, come se avesse a disposizione un “collaboratore elettronico” dotato di una notevole
“intelligenza”. Da qui, purtroppo, hanno origine molti problemi interpretativi, false aspettative e disillusioni
sugli Elaboratori: se da una parte l’Elaboratore diventa più “vicino all’uomo”, dall’altra presenta potenziali
“imperfezioni”, dovuti all’intervento di un altro “uomo” ovvero del programmatore, che possono vanificarne
l’utilizzo dando risultati errati ed inattesi. Se l’utente ottiene risultati “errati”, la sua visione lo porta a
considerare colpevole “l’elaboratore” nel suo complesso, dimenticando troppo spesso che il suo problema lo
ha esposto originariamente ad un uomo, non alla macchina.
Da queste poche considerazioni sintetiche, si intuisce che, se si riesce ad ridurre al minimo la distanza tra
linguaggio macchina e linguaggio umano, si avranno sempre meno risultati errati e quindi un Elaboratore
sempre più vicino alla sua definizione immaginifica di “collaboratore elettronico”.
Tale distanza, come visto sopra, dipende dal tipo di utente: se l’utente si avvicina alla definizione che
abbiamo sopra dato di “programmatore”, questa sarà molto piccola, altrimenti, tale distanza può crescere
enormemente, quando si richiede agli elaboratori di risolvere problemi sempre più complessi e con
un’interfaccia “utente” sempre più semplice, ovvero si richiede all’elaboratore una maggiore "efficacia".
Per cercare di superare questa divergenza notevole tra uomo e macchina, è necessario avere dei SISTEMI
DI ELABORAZIONE molto complessi, sia da realizzare che da studiare, se consideriamo la macchina solo
come insieme di circuiti elettronici digitali.
Per trattare tale complessità, è necessario affrontare lo studio, la progettazione e la realizzazione di tali
sistemi con un approccio strutturale, organico e sistematico, isolando e nascondendo opportunamente le
complessità non rilevanti da quelle significative e superando quindi il concetto di macchina come “insieme di
circuiti” verso un concetto di “macchina virtuale” ovvero di una macchina che sembri (o emuli) un “insieme di
circuiti” ma che è costituita da hardware e software, semplificando quindi la sua stessa realizzazione.
In questo testo, viene usato tale approccio sistematico, individuando e tracciando delle frontiere tra il sistema
ed il mondo esterno in modo opportuno ovvero cercando di evidenziare quegli elementi che sono significativi
e cercando di nascondere quelli che porterebbero solo "confusione" per la descrizione completa del sistema
stesso.
5
Affrontare l’architettura degli elaboratori si riduce quindi nell’affrontare l’architettura di questa frontiera che
indichiamo quindi semplicemente come l’interfaccia attuale uomo-macchina
Esistono molte possibilità di tracciare la frontiera tra il sistema e il mondo esterno e per ognuna di esse,
possiamo individuare dei sottoinsiemi funzionali di diversa natura.
La scelta seguita in questo testo, identifica formalmente dei sottosistemi strutturati a livelli gerarchici che
chiameremo "Macchine Virtuali".
L'uso di livelli gerarchici, ci permette, oltre alla facile individuazione delle relazioni che intercorrono tra di
essi, di poter GENERALIZZARE e ASTRARRE il funzionamento a quel livello specifico, senza curarci di
come siano costituiti i livelli inferiori.
Dal punto di vista del programmatore si hanno notevoli vantaggi e semplificazioni: Ad esempio, si potrebbe
scrivere un programma in un determinato linguaggio indipendentemente dalla particolare macchina utilizzata
ovvero trascurando le sue caratteristiche costruttive.
In generale, una strutturazione a livelli, o MACCHINE VIRTUALI può essere vista secondo il seguente
schema:
Tale modello mette in evidenza un aspetto fondamentale che occorre tener presente: i livelli sono
“impermeabili” ovvero dobbiamo considerare ciascun livello come un “manto” che circonda completamente il
livello sottostante e quindi ne controlla completamente l’accesso.
In questa strutturazione a livelli, i due livelli estremi evidenziano delle particolarità sui Linguaggi, rispetto alla
definizione generale sopra data.
6
In particolare, il livello più basso, il livello che chiamiamo MV 0, indica la macchina reale, ovvero quella
costituita effettivamente da un insieme di circuiti elettronici digitali (detta anche “Hardware”), è costituito si da
un insieme di risorse (i circuiti) ma non ha un vero e proprio linguaggio in quanto le funzionalità dei circuiti
sono insite nella struttura stessa ovvero sono dettate da precise leggi fisiche, le leggi di Maxwell
(eventualmente semplificate ed approssimate).
Anche il livello più alto, quello che vede l’utente, dispone di risorse ma non di un linguaggio vero e proprio: in
questo caso le funzionalità delle risorse sono predefinite dal programmatore ed è possibile accedere a
queste tramite un’interfaccia di interazione costituita, ad esempio, da pulsanti, menù, maschere di
input/output. Per superare queste eccezioni, presenti anche ai livelli inferiori, introduciamo il concetto di
“Linguaggio generalizzato” che comprende cioè sia Pacchetti Applicativi, sia i linguaggi di programmazione,
sia i Sistemi di comunicazione con altri sistemi e/o strumenti.
Tale definizione dei Linguaggio consente una maggiore flessibilità anche per tutti i livelli intermedi MVi, dove
il linguaggio Li consente di controllare e gestire le risorse Associate al Livello Ri: tali risorse appaiono come
“primitive” di quel livello ma sono in genere strutturate e composte con primitive dei livelli inferiori.
Le relazioni tra i vari livelli, sono di tipo GERARCHICO: per salire nella gerarchia, occorre astrarre ovvero
costruire delle risorse “virtuali che compariranno come “primitive” al livello superiore; per scendere di livello,
occorre concretizzare ovvero scomporre le primitive di quel livello negli elementi del livello inferiore
(emulazione).
0 <j<i
normalmente sarà j=i-1 ma, per ragioni di efficienza, può essere anche minore, ovvero la particolare politica
può far uso di primitive appartenenti ad uno qualsiasi dei livelli sottostanti.
Se ne deduce, che, individuando meccanismi distinti di Li, questi possono essere implementati utilizzando
linguaggi appartenenti anche a dei livelli differenti.
E' facile a questo punto immaginare come sia possibile, aumentando a piacere il numero di livelli, ottenere
una MVn con un linguaggio che possa soddisfare le necessità dell'utilizzatore.
E’ facile anche immaginare come, salendo di livello, aumenti l’efficacia, intesa come “produttività” ovvero la
capacità di ottenere il risultato voluto con il minimo di risorse, e la flessibilità del sistema, a spese di una
minore efficienza, intesa come “velocità” di elaborazione espressa in numero di istruzioni per unità di tempo.
Vediamo ora concretamente come possono essere realizzate queste macchine virtuali a livelli, usando le
tecniche di traduzioni basate sulla compilazione ed interpretazione.
La strutturazione a livelli diventerà molto più chiara se si analizzeranno in dettaglio i processi di compilazione
ed interpretazione.
Per Compilazione PURA, si intende l'operazione attraverso la quale si sostituisce al programma originario,
scritto in linguaggio di alto livello, più vicino all’uomo (programma sorgente) un programma funzionalmente
equivalente scritto in linguaggio di basso livello, più vicino alla macchina (codice binario o programma
oggetto). Una volta trasformato in linguaggio di basso livello il programma può essere caricato nella
macchina e quindi eseguito.
Per far eseguire un programma, usando la compilazione è quindi necessario suddividere il processo in due
fasi distinte: compilazione ed esecuzione, secondo la seguente figura:
7
Programma Programma Dati Risultati
COMPILATO PROGRAMM
Sorgente RE Oggetto A
OGGETTO
FASE DI COMPILAZIONE FASE DI ESECUZIONE
Per concretizzare un livello, possiamo utilizzare, sia la compilazione che l’interpretazione, tenendo però
presente i rispettivi vantaggi e svantaggi sintetizzati nello schema seguente:
Compilazione Interpretazione
a) Velocità di esecuzione più alta che a) Possibilità di effettuare modiche al
nell’interpretazione (alta efficienza) programma immediatamente
Vantaggi
Per avere i vantaggi di ambedue le soluzioni, conviene adotta adottare una soluzione mista, definendo un
linguaggio intermedio (tra uomo e macchina) ovvero ottimizzato per una rapida ed efficiente interpretazione
da parte di un programma, chiamato “run time executor” o semplicemente “run time”. Il Compilatore, in
questo caso, dovrà semplicemente tradurre da linguaggio di alto livello nel linguaggio intermedio ottenendo
tutte le garanzie di eliminazione degli errori sintattici date dalla compilazione. Nel caso si debbano costruire
molti programmi, si avrà inoltre un’ulteriore vantaggio dovuto al minore ingombro dei programmi scritti in
linguaggio intermedio rispetto agli analoghi scritti in codice binario, in quanto le primitive ricorrenti saranno
contenute nel Run-Time. (in realtà, anche i programmi in codice binario possono avere degli ingombri limitati
se si utilizza la tecnica delle librerie “condivise”). Anche qui, come per i compilatori, è possibile utilizzare
linguaggi diversi che producano lo stesso linguaggio intermedio e quindi utilizzino un unico run-time,
consentendo al programmatore di scrivere programmi per una stessa procedura, utilizzando il linguaggio che
più si adatta per ogni situazione.
A questo punto, vediamo come dovrebbero essere realizzati i procedimenti di traduzione (procedimento
statico: ogni componente al livello i, viene SOSTITUITO da un insieme di componenti al livello j) o
interpretazione (procedimento dinamico: non avviene la sostituzione , ma possono coesistere elementi
8
appartenenti al livello i ed al livello j) che permettono di concretizzare un livello ovvero di definire i
meccanismi al livello i tramite politiche al livello j.
Tenendo a mente la definizione di “linguaggio generalizzato” e la relazione gerarchica tra i livelli, possiamo
associare, in definitiva, ad ogni livello virtuale MVi un "supporto a tempo di esecuzione" (run-time-support) di
Li, che chiameremo RTS(Li). Un RTS(Li) consiste in una libreria di algoritmi e strutture dati, implementati u-
tilizzando l'insieme dei livelli Mvj (0<j<i), atti ad eseguire i meccanismi di Li.
Possiamo, a questo punto, dare una prima definizione formale di ARCHITETTURA di un SISTEMA di
ELABORAZIONE, riferendoci alla frontiera tra il livello considerato ed il mondo esterno (livelli superiori)
ovvero alle sue caratteristiche: risorse, pacchetti applicativi, linguaggi di programmazione, sistemi di
comunicazione.
Livello 4
Livello 3
Livello 2
Livello 1
Livello 0
9
MV2: Livello ISA o livello “Assembler” o “Architettura”
R2= Locazioni di memoria, Registri generali e di I/O
L2= Linguaggio Macchina
Questo livello viene normalmente documentato in letteratura e dai costruttori come "architettura" del
sistema ed è quindi caratterizzato da un linguaggio utilizzabile dall'utente (sia pure specializzato). Per
lavorare con la MV2 si dovrà conoscere in maniera approfondita quello che viene indicato come
"Programming Model" ovvero l'insieme delle regole per l'utilizzo della macchina: Istruzioni, registri, tipi dei
dati, modalità di indirizzamento della memoria, etc.
La traduzione verso il livello MV1 viene effettuata da un interprete chiamato "microprogramma"
Ulteriori suddivisioni in sottolivelli potrebbero essere fatte per MV 4. tipicamente tale strato viene così
suddiviso: dello strato applicativo sono i seguenti:
Sottolivello Descrizione
MV4.3 Sono programmi “speciali” che gestiscono coerentemente transazioni ovvero
APPLICAZIONI trasferimenti di dati o importi. Una tipica transazione è quella di tipo
TRANSAZIONALI: commerciale dove, a fronte di un pagamento, un conto corrente deve essere
decrementato ed un altro deve essere incrementato di pari importo, con la
garanzia che l’operazione non posa interrompersi a metà ovvero se vi sono
problemi si deve riportare il sistema alla condizione iniziale (Rollback)
MV4.2 Per poter scrivere applicazioni transazionali è necessario disporre di un
GESTIONE gestore di transazioni che metta a disposizione le primitive indispensabili quali:
DI TRANSAZIONI “Begin transaction”; “Commit”; “Rollback”
MV4.1 Le applicazioni transazionali ed il gestore delle transazioni hanno come
GESTIONE oggetto una base dati che sovrintende l’organizzazione e la scrittura dei dati
DI DATA BASE su supporto permanente. Ad esempio, la primitiva “Commit” dovrà essere
interpretata da questo livello come rendere definitivi gli item temporanei creati
a partire da “Begin transaction”.
MV4.0 E’ questo il livello più basso e che era stato indicato come MV4. In effetti viene
LINGUAGGI ridimensionato il concetto di “linguaggio di alto livello” limitandolo solo a quelle
operazioni che non debbano gestire “basi dati”: queste ultime dovranno infatti
essere eseguire con linguaggi di più alto livello, che si pongano almeno al di
sopra del MV4.1
Tabella 1.2 - Sottolivelli tipici della MV4
Sottolivello Descrizione
MV3.2 Il livello più esterno del Sistema Operativo è quello che deve orchestrare
10
FUNZIONI l’utilizzo e condivisone delle risorse da parte degli strati superiori o da parte
DI SUPERVISORE dell’utente.
MV3.1 Le risorse vanno comunque gestite con primitive opportune in modo da
GESTIONE semplificarne l’utilizzo da parte dei livelli superiori. Rientrano in questa
ALTRE RISORSE categoria i driver di periferica.
MV3.0 Le risorse fondamentali, processi (intesi come segmenti di codice macchina in
GESTIONE esecuzione) e processori devono essere gestite separatamente in quanto tutti
PROCESSI/PROCESSORI le altre risorse dovranno inevitabilmente ricorrere a loro.
Tabella 1.3 - Sottolivelli tipici di MV3
L'equivalenza è solo logica e non pratica: pietrificare in hardware del software è un'operazione comunque
costosa e conviene valutare bene prima l'opportunità economica di tale operazione.
Fu proprio l'aspetto economico che spinse M.Wilkes, ricercatore presso l'Università di Cambridge, nel 1951,
a progettare un sistema a tre livelli: livello ISA, livello Microprogrammazione e livello Digitale.
Il livello digitale doveva eseguire microistruzioni ovvero supportare un set di istruzioni ridotto rispetto a quelle
disponibili a livello ISA.
Il microprogramma, inserito dal costruttore in forma non modificabile dall'utente, colmava il gap tra il livello I
ed il livello logico, usando degli opportuni algoritmi che andavano ad incidere sul data path.
Fi = {fi1,fi2,....fik}
L'insieme delle "FUNZIONI" assegnate (da intendere il termine funzione in senso molto ampio, appunto
come 'funzionalità') a MVi. Da questo insieme, individuiamo dei sottoinsiemi F ijj disgiunti di Fi, che
indicheremo con:
n n
Fi U Fij
j 1
con F
j 1
ij
3
Paragrafo [1.1.3] del libro “Structured Computer Oranization” – Andrew S. Tanenbaum
11
Questa decomposizione dipende dalle prestazioni che si vogliono ottenere a quel livello e sara' tipica della
sua architettura.
Possiamo, a questo punto definire formalmente il SISTEMA DI ELABORAZIONE AL LIVELLO MVi come :
Mi1
Mi2
STRUTTURA
DI
INTERAZION
E Mij
Fig. 1.5.1
UNITA' DI ELABORAZIONE
P.C. x Rete y
Combinatoria
condizioni comandi s s’
x P.O. y Buffer
clock
12
Fig 1.5.2 Fig 1.5.3
Esse sono costituite da due AUTOMI che indicheremo con PC (Parte di Controllo) e PO (Parte Operativa),
per distinguere la sezione di controllo da quella operativa, interconnessi come in Fig. 1.5.1.
Ogni funzione affidata all'unità di elaborazione, viene interpretata da un microprogramma. La PO, esegue le
operazioni elementari previste da ogni microistruzione utilizzando, ad esempio:
L'esecuzione di ogni microistruzione, è controllata dalla PC, la quale invia dei segnali di comando che vanno
ad agire sulle risorse di PO. La PC conosce lo stato della PO, tramite il valore dei segnali detti "variabili di
condizionamento".
Sia PC che PO, sono implementati come delle reti sequenziali sincrone di tipo LLC (Fig. 1.5.2 e 1.5.3),
cioè con segnali di ingresso e di uscita a Livelli (L-L) e sincronizzate da un segnale periodico impulsivo detto
"clock" dell'unita (C).
Gli AUTOMI (Macchine sequenziali sincrone) possono essere realizzati secondo due diversi modelli
matematici:
MODELLO DI MEALY:
secondo il quale, l'uscita e lo stato successivo dipendono sia dallo stato interno che dai valori degli ingressi.
MODELLO DI MOORE:
𝑌(𝑡) = 𝐹𝑦 [𝑆(𝑡)] 𝐹𝑦 = 𝐹𝑢𝑛𝑧𝑖𝑜𝑛𝑒 𝑑𝑖 𝑈𝑠𝑐𝑖𝑡𝑎
{
𝑆(𝑡 + 1) = 𝐹𝑠 [(𝑋(𝑡), 𝑆(𝑡)] 𝐹𝑠 = 𝐹𝑢𝑛𝑧𝑖𝑜𝑛𝑒 𝑑𝑖 𝑆𝑡𝑎𝑡𝑜
secondo il quale, l'uscita dipende solo dal valore dello stato presente, mentre lo stato successivo dipende sia
dallo stato interno che dai valori degli ingressi. In questo caso, notiamo come la variazione degli ingressi
all'inizio del ciclo, influenzerà l'uscita solo al prossimo ciclo del clock.
13
2. La rappresentazione dell’informazione (A)
L'informazione è, per sua natura, un'entità astratta ed ha quindi bisogno di essere rappresentata tramite
grandezze fisiche concrete. Una volta trasformata in un mezzo omogeneo, è possibile far operare le diverse
unità che compongono i calcolatori attraverso i dispositivi di memorizzazione e trasmissione che co-
stituiscono la RETE LOGICA DI FLUSSO DEI DATI (data path).
Nei calcolatori digitali, occorre rappresentare le informazioni usando livelli discreti di una grandezza fisica
(tensione, corrente, flusso magnetico, etc.). Ad ogni livello è possibile associare un simbolo ben preciso;
l'insieme costituito da questi simboli costituisce l'ALFABETO tramite il quale le informazioni possono essere
espresse.
Per memorizzare l'informazione così trasformata, occorreranno dei dispositivi in grado di assumere le N
configurazioni diverse relative ad ogni simbolo dell'alfabeto. Un tale dispositivo viene chiamato
DISPOSITIVO ELEMENTARE a N STATI.
L'alfabeto usato nei calcolatore digitali è costituito da DUE SIMBOLI che chiameremo nel seguito 0 e 1, ai
quali corrisponderanno due livelli discreti della grandezza fisica in grado di rappresentare la configurazione
del dispositivo elementare a 2 stati necessario per la memorizzazione dell'informazione.
La scelta di un alfabeto a due simboli può essere motivata dalle seguenti esigenze:
EFFICIENZA: Le operazioni matematiche in binario sono molto veloci e questo rende il calcolatore molto
efficiente nelle applicazioni scientifiche.
SEMPLICITÀ: I circuiti necessari per operare in aritmetica binaria sono notevolmente semplici (es.: full-
adder) e ciò comporta un minor costo della macchina che può essere realizzata sfruttando grosse
economie di scala. (esistono molti circuiti replicati tutti uguali tra di loro).
AFFIDABILITÀ: Un calcolatore deve dare risultati esenti da errori ( ovvero con probabilità di errore il più
possibile bassa). Lo scelta dei due livelli permette di aumentare l' affidabilità della macchina in quanto,
utilizzando degli opportuni dispositivi di trigger, questa può essere resa insensibile ad eventuali rumori o
disturbi. Ad esempio, se si associa al livello 0 il valore di tensione -12V e al livello 1 il valore di tensione
+12V, si possono prevedere di approntare degli opportuni trigger in grado di riconoscere:
- livello 0 qualsiasi valore di tensione compreso tra -2 e -22V
- livello 1 qualsiasi valore di tensione compreso tra +2 e +22V
questo significa, che eventuali disturbi di entità compresa tra -10V + 10V sovrapposti al livello nominale,
non alterano l'informazione. La fascia di indecisione compresa tra -2V e +2V, può essere rimossa
assumendo come stato valido il valore dell’ultimo livello riconosciuto. Alternativamente, si può adottare la
scelta di segnalare un errore, nel caso ci si trovi ad interpretare un livello di tensione appartenente alla
fascia di indecisione.
Nella figura che segue, viene rappresentato un esempio di come un segnale disturbato può essere
ricostruito tramite l’impiego di trigger
- Metodo posizionale
Individuare la maggiore potenza del 2 contenuta nel numero, sottrarla al valore in questione e proseguire
fino a che il resto sarà 0 o 1.
Ad esempio, per convertire in binario il numero decimale 27, troviamo che la massima potenza del 2
4
contenuta è 16 = 2 , quindi possiamo iniziare a scrivere un 1 in QUINTA posizione e calcoliamo 27-16=11.
3
La massima potenza del 2 contenuta in 11 è 8=2 , quindi scriviamo un 1 anche in QUARTA posizione e
calcoliamo 11-8 = 3.
1
La massima potenza del 2 contenuta in 3 è 2 = 2 , quindi scriviamo 0 in TERZA posizione, 1 in SECONDA
posizione e calcoliamo 3-2 =1.
A questo punto il resto è < 2 e quindi possiamo scriverlo direttamente in PRIMA posizione: quindi il risultato
della conversione del numero decimale 27 sarà 11011
1 118 2
0 59 2
1 29 2
11101101
1 14 2
0 7 2
1 3 2
1 1 2
1 0
L’aritmetica BINARIA segue le stesse regole di quella DECIMALE e di qualsiasi sistema posizionale
analogo.
L’unica eccezione da tener conto è che nell’aritmetica binaria, i simboli sono solo 0 e 1. Per chiarire il
concetto, effettuiamo alcune operazioni di esempio:
50 - 110010 - Per la differenza, vale lo stesso discorso. In questo caso occorre considerare,
27 = 11011 = al posto dei riporti, gli eventuali prestiti e scalarli alla cifra più a sinistra ovvero
alla cifra più significativa.
15
23 10111
11011
Per poter effettuare dei calcoli aritmetici, l’informazione elementare, deve essere strutturata fisicamente, in
modo da consentire la rappresentazione di ogni informazione in modo opportuno nelle celle di memoria.
Per accedere ad ogni cella di memoria occorre disporre del relativo indirizzo. Tutte le celle di memoria
contengono lo stesso numero di bit: una cella composta da k bit è in grado di contenere una qualunque delle
k
2 combinazioni diverse di bit.
Concretamente, l’informazione viene collocata in strutture gerarchiche, di dimensione crescente, nel
seguente modo:
Nell’uso comune, sono state individuate le seguenti strutture ulteriori, basate sul numero di valori possibili
immagazzinabili:
Nota:
La definizione di WORD, risulta ambigua, in quanto si riferisce a due possibili interpretazioni. Per risolvere
quatta ambiguità, useremo per convenzione il termine “PAROLA” per indicare l’unità fondamentale di
informazione ed il termine “WORD” per indicare un gruppo di 16 bit.
Dalle strutture fisiche di informazione si nota che le strutture gerarchicamente superiori possono essere
espresse come multipli delle strutture inferiori ovvero:
16
sistema di numerazione ottale, a base 8, utilizzando le cifre decimali da 0 a 7.
La seguente tabella riporta la corrispondenza tra i codici binari e quelli dei sistemi intermedi legati a strutture
fisiche di aggregazione:
Da tale tabella risulta evidente che il sistema di binario, pur avendo i vantaggi (per la macchina) espressi
precedentemente, risulta scomodo e non maneggevole per noi: basti pensare che per rappresentare
completamente un numero di 5 cifre decimali, occorrono ben 17 cifre binarie. Usare quindi i sistemi di
numerazione a base multipla del 2 consente di superare la scomodità di manipolazione, oltre che avere una
immediata conversione due sensi.
a) MODULO E SEGNO
È il modo più semplice e intuitivo, si utilizza il bit più significativo della struttura fisica per indicare, con 0 un
numero positivo (+) e con 1 un numero negativo (-). Anche se più immediata, non è però questa la
rappresentazione consistente con le operazioni aritmetiche eseguibili con i circuiti logici, come ad esempio i
“full-adder”.
Da tale rappresentazione appare evidente una possibile ambiguità: ad esempio il valore 80H vale -128 nella
prima riga e +128 nella seconda. Questa ambiguità può essere rimossa solo se viene definita rigorosamente
l'ampiezza della parola, ovvero il numero di bit da usare per rappresentare il numero relativo.
La rappresentazione in complemento a 2 ha il vantaggio poter effettuare le operazioni aritmetiche tra numeri
relativi in modo diretto: sommando due numeri relativi, si ottiene immediatamente, trascurando per il
momento il riporto, il risultato corretto (es.: (+5) + (-3) = 05h + FDh = 02h = 2). Per effettuare una sottrazione,
17
si può quindi usare un addizionatore, avendo cura di fare il complemento alla base del numero da sottrarre
(es.: 15 - 8 = 0Fh - 08h = 0Fh + F8h = 07h = 7 ).
Per eseguire il complemento alla base (complemento a 2) basta semplicemente invertire (= complemento a
1) tutti i bit del numero e sommare 1 (es.: -20 = -(14h) = -(00010100b) = 11101011b+1 = 11101100b = ECh).
ris = op1 + op2: (S(op1)=0) E (S(op2)=0) E (S(ris)=1) OPPURE (S(op1)=1) E (S(op2)=1) E (S(ris)=0)
ris = op1 + op2: (S(op1)=0) E (S(op2)=1) E (S(ris)=1) OPPURE (S(op1)=1) E (S(op2)=0) E (S(ris)=0)
N = b^e * m (b = base = 2)
La mantissa viene espressa in forma "normalizzata" ovvero nella forma per la quale tutte le sue cifre sono
significative.
Il formato di un numero binario in virgola mobile viene espresso comunemente nel seguente modo:
18
[S][E][M]
con
S = Segno della mantissa
E = Esponente secondo la rappresentazione in complemento a 2
M = modulo della mantissa.
Il numero di bit da riservare per ogni campo della stringa di bit cosi' composta è variabile e dipende dalla
precisione richiesta.
E’ evidente che, se per la rappresentazione dei numeri interi si potevano avere dei problemi di
“trasportabilità” tra le varie architetture, per la rappresentazione dei numeri reali se ne hanno ancora di più.
Esempio:
Per rappresentare il numero 10111,0011b, occorre prima normalizzarlo in 0,101110011b * 2^5 e quindi
avremo, in singola precisione:
In realtà, la maggior parte delle architetture ormai si è adeguata allo standard ANSI/IEEE Std. 754-1985
conosciuto anche semplicemente come IEEE Standard for Binary Floating-Point Arithmetic
Secondo questa rappresentazione, quindi, un numero reale normalizzato (ovvero con 0 <= M < 1) è
composto dai tre campi [S][E][M] ed è matematicamente determinato dalla seguente formula:
(1) S * 1 M ) * 2 E
tenendo presenti i seguenti casi speciali:
lo Zero può avere il bit S = 0 o S = 1, purché sia E che M siano = 0
Se E=0 e M >0 allora il numero è considerato come “Subnormale” ovvero più piccolo (in modulo) di
quanto rappresentabile con quel formato.
Infinito è indicato da M=0 ed E con tutti “1”, mentre se M>0 non è accettabile come numero.
Il più piccolo valore rappresentabile (tale che, se sommato ad 1 da un valore maggiore di 1) viene indicato
come “Machine Epsilon” e vale: MacEps 2 p dove p è il numero di bit dedicati alla Mantissa.
Nome Formato Min Normal Max Normal Min Subnormal Machine Epsilon
Single 1.175e-38 3.403e+38 1.401e-45 1.192e-07
Double 2.2e-308 1.8e+308 4.9e-3246 2.220e-16
19
Extended <3.4e-4932 >3.4e+4932 <3.6e-4951 <1.084e-19
Quadruple 3.4e-4932 3.4e+4932 6.5e-4966 1.926e-34
Una delle scelte progettuali da evidenziare e quella della Burrougs in quanto, a livello MV1, prevedeva uno
spazio di indirizzi a 19 bit per indirizzare ciascun singolo bit, ovvero uno spazio di indirizzi a 15 bit per
indirizzare della parole da 16 bit.
La necessità di superare i problemi di comunicazione delle informazioni tra computer di diversi fornitori ha
portato a definire degli standard per le dimensioni e la scalabilità delle strutture fisiche.
2.3 Composizione delle strutture fisiche di informazione
Tra le strutture gerarchiche di informazione, quella più largamente utilizzata attualmente, anche come unità
di misura, è senza dubbio il BYTE: è una prassi comune definire la quantità di memoria disponibile in un
computer esprimendola in BYTES, anche se l’unità fondamentale di informazione (PAROLA) dei computer
attuali è la DOUBLE WORD (32 bit).
Il motivo di tale scelta è originato principalmente dal fatto che, come vedremo in seguito, la dimensione di un
byte ci permette di esprimere 256 combinazioni diverse, sufficienti per rappresentare ampiamente un
carattere dell’alfabeto (maiuscoli, minuscoli, segni di interpunzione e simboli speciali) e quindi adatto a
contenere un’informazione alfanumerica. Un’altra ragione è di motivo storico, dettata dall’evoluzione dei
microprocessori da 8 a 16 e quindi a 32 bit: le strutture fisiche si sono evolute tenendo come riferimento il
byte.
Si tratta, a questo punto, di decidere come deve essere rappresentata una struttura gerarchicamente
superiore accostando più bytes. Si hanno, in particolare due possibilità:
1. Big Endian: il primo byte di una word (o double word o quad word) indica la cifra più significativa
2. Little Endian: il primo byte di una word (o double word o quad word) indica la cifra meno significativa
La prima è più vicina alla nostra rappresentazione dei numeri: per indicare il numero “centododici” in fatti noi
indichiamo prima le centinaia, poi le decine e quindi le unità. Questo tipo di rappresentazione era quella
utilizzata principalmente nei computer degli anno 60/70: leggendo sequenzialmente la memoria di un
20
computer si intuiva facilmente il valore di un numero composto da più bytes
La seconda, che è stata introdotta la prima volta nei PDP-11, ma è più conosciuta come “Formato INTEL”, è
più vicina alle esigenze dei microprocessori ed è auto allineante. Infatti, se si deve effettuare una somma tra
due numeri, costituiti ciascuno da un numero di bytes variabile, occorre effettuare le somme parziali a partire
dalla cifra meno significativa fino alla più significativa: se i numeri sono rappresentati in Little endian basta
partire dal primo byte e proseguire in avanti finché uno dei due non termina. Viceversa, se i numeri sono
rappresentati in Big endian occorre prima allineare a destra i due numeri e quindi eseguire il calcolo
spostandosi all’indietro.
Quindi, nelle due possibilità si hanno dei vantaggi e svantaggi circa equivalenti. A conferma di ciò, basti
considerare, ad esempio, che nei processori INTEL x86, in cui si addotta il Little-endian in memoria, i registri
sono in formato Big-endian.
I termini Little endian e Big endian derivano, storicamente dai “I viaggi di Gulliver” di Swift:
A Lilliput si emanò una legge che obbligava i cittadini a rompere le uova dalla parte più piccola (Little
endian); questo scatenò una guerra civile tra quelli che volevano rompere le uova dalla parte più grande (Big
endian). I Big-endian furono quindi costretti a rifugiarsi in un isola vicina, il regno di Blefuscu. (Da notare che
Big-endian ha la stessa iniziale di Blefuscu e Little-endian quella di Lilliput)
La satira di Swift, ovviamente si riferiva alla guerra religiosa (assurda) tra l’Inghilterra Protestante e la
Francia Cattolica e metteva in evidenza la futilità del contendere.
Anche nel nostro caso, non è il caso di scatenare una guerra, ma una decisione deve essere pur presa se si
vogliono far comunicare computer diversi tra di loro.
Purtroppo, ancora oggi non si è presa una decisione e nessun “saggio” ha emanato un proclama che obbliga
i costruttori ad utilizzare un metodo piuttosto che l’altro con il risultato di avere una “costosa anarchia”:
quando si ha la necessità di collegare due sistemi diversi, occorre informarsi sul tipo di endian (allineamento)
utilizzato da ciascuno e provvedere a scrivere le opportune routine di conversione, inserendo quindi un
overhead (sovralavoro) nel caso che i due sistemi non usino lo stesso allineamento.
21
Chiamando queste combinazioni PAROLE DEL CODICE, possiamo formalizzare le seguenti definizioni:
CODIFICA : operazione che ci permette di associare ogni elemento dell'alfabeto esterno con una parola
del codice.
DECODIFICA: operazione che ci permette di associare ogni parola del codice con un elemento dell'
alfabeto esterno.
è chiaro che, per poter effettuare correttamente la codifica e la decodifica, dovremo utilizzare dei CODICI
NON AMBIGUI. Per questo, dovremo trovare il numero minimo di simboli necessari affinché un codice non
sia ambiguo. Nel caso di calcolatori digitali, dove si hanno a disposizione i due simboli 0 e 1, dovrà essere,
indicando con n il numero di bit necessario e con N il numero di simboli da codificare ( appartenenti
all'alfabeto esterno):
La disuguaglianza precedente porta evidentemente ad una ridondanza nel caso che sia n > m, ovvero
avremo dei bit in più del necessario per la non ambiguità.
Il numero di bit di cui differiscono due parole qualsiasi del codice, viene chiamata DISTANZA DI
HAMMING: da quanto detto sopra avremo quindi che, per codici irridondanti avremo h = 1 (n = m), mentre
per codici ridondanti avremo h>1 ( n>m ovvero n - m bit in più).
L'uso di codici RIDONDANTI ha una grande importanza pratica in quanto ci permettono di riconoscere o
addirittura correggere eventuali errori, che possono avvenire durante la fase di trasmissione, dovuti ad una
inversione di uno o più bit. Possiamo dire che i codici con h>1, possono rivelare, al massimo, errori su h-1 bit
di ogni parola del codice. Ad esempio, se abbiamo h=3, avremo che ogni parola del codice differisce da
un'altra per 3 bit, quindi tra una parola e l'altra, avremo due configurazioni che sono errate: queste due
configurazioni sono tutte quelle che si possono ottenere invertendo, al massimo, due bit.
Se un codice ha h>=3 possiamo pensare di correggere eventuali errori, scegliendo, come possibile la
configurazione più vicina ad una ammessa dal codice. Il numero di bit errati che è possibile correggere,
dipende dal valore di h, come vedremo più in dettaglio nel paragrafo seguente.
è un codice irridondante (h=1) per codificare le dieci cifre decimali (da 0 a 9) con 4 bit, secondo il sistema di
numerazione binario:
22
CIFRA BCD CIFRA BCD
0 0000 5 0101
1 0001 6 0110
2 0010 7 0111
3 0011 8 1000
4 0100 9 1001
L'applicazione frequente di questo codice si ha quando si debba operare nel sistema decimale in maniera
agevole e interpretabile.
É immediato pensare infatti che con 8 bit a disposizione, si ha la possibilità di memorizzare due cifre BCD: in
questo caso, il codice verrà chiamato più propriamente "packed BCD", per mettere in evidenza che un byte
può contenere due cifre decimali, al contrario del codice "unpacked BCD" dove comunque rimangono inu-
tilizzati 4 bit (vedi successivi codici ASCII e EBCDIC).
Nei casi in cui, è necessario mantenere costante la distanza di hamming tra due codici successivi, occorre
utilizzare una successione non binaria. è questo il caso di contatori che trasmettono su delle linee dove è
necessario ridurre al minimo la generazione di armoniche che possono produrre errori. Infatti, mantenendo
costante la distanza di hamming, non si corre il rischio di passare da una configurazione in cui quasi tutti i bit
sono a 1, ad una dove solo un bit è a 1 (es.: da 0111 a 1000) che può produrre delle armoniche di energia
consistente e quindi la possibilità di errori dovuti agli accoppiamenti induttivi e capacitivi tra i conduttori.
Il codice GRAY a due bit sarà quindi:
00 01 11 10
Per passare dal codice GRAY a n bit a quello a n+1 bit, basta usare la seguente regola pratica (da cui il
nome di codice RIFLESSO):
riscrivere il codice a n bit in forma speculare, ovvero dall'ultima configurazione alla prima.
premettere uno 0 davanti alle vecchie configurazioni e un 1 davanti alle nuove configurazioni.
Ad esempio, ricaviamo il codice GRAY a 3 bit, da quello a 2 bit: riscrivo il codice riflesso:
10 11 01 00
combino vecchio e nuovo codice:
000 001 011 010 110 111 101 100
E' un codice adottato essenzialmente per trasmissione dati e soprattutto per applicazioni gestionali. Esso è
una estensione del codice BCD, per poter rappresentare tutti i simboli dell'alfabeto esterno: I digit da 0 a 9
hanno i 4 bit più a sinistra fissi a 1.
É un codice binario a sette bit, in grado di rappresentare 128 simboli diversi. Questo codice, pubblicato nel
1968 come ANSI X3.4, è stato il più usato, sia per la rappresentazione dei dati, sia per lo scambio di
informazioni tra sistemi diversi.
b6-b4 0 1 2 3 4 5 6 7
(000) (001) (010) (011) (100) (101( (110) (111)
b3-b0
0 (0000) NUL DLE SP 0 @ P ` p
1 (0001) STX DC1 ! 1 A Q a q
23
2 (0010) SOT DC2 “ 2 B R b r
3 (0011) ETX DC3 # 3 C S c s
4 (0100) EOT DC4 $ 4 D T d t
5 (0101) ENQ NAK % 5 E U e u
6 (0110) ACK SYN & 6 F V f v
7 (0111) BEL ETB ‘ 7 G W g w
8 (1000) BS CAN ( 8 H X h x
9 (1001) TAB EM ) 9 I Y i y
A (1010) LF SUB * : J Z j z
B (1011) VT ESC + ; K [ k {
C (1100) FF FS , < L \ l |
D (1101) CR GS - = M ] m }
E (1110) SI RS . > N ^ n ~
F (1111) SO US / ? O _ o DEL
NUL Carattere Nullo TAB Horizontal Tabul. DC2 Device Control 2 ESC Escape
STX Start Transmission LF Line Feed DC3 Device Control 3 FS Frame Separator
SOT Start Of Text VT Vertical Tabulation DC4 Device Control 4 GS Group Separator
ETX End Of Transmiss. FF Form Feeed NAK Negative Acknow. RS
EOT End Of Text CR Carriage Return SYN Synchronize US
ENQ Enquiry SI Shift In ETB SP Spazio
ACK Acknowledge SO Shift Out CAN Cancel DEL Delete character
BEL Bell (campanello) DLE EM End Mark
BS BackSpace DC1 Device Control 1 SUB
Tabella dei codici ASCII
Strutturalmente, può essere considerato come costituito da 4 parti, ciascuna da 32 combinazioni: la prima
(da 00 a 1F) è riservata a dei caratteri di controllo, ovvero a dei caratteri che assumono significato a seconda
dei in determinato protocollo o formato di dati. La seconda (da 20 a 3F) è riservata ai segni di interpunzione,
aritmetici e logici, nonché alla rappresentazione delle 10 cifre decimali da 0 a 9 (da 30 a 39).
Le ultime due parti sono riservate alla rappresentazione delle lettere dell'alfabeto MAIUSCOLE e
MINUSCOLE (da 40 a 5F e da 60 a 7F rispettivamente). Ovviamente, essendo 26 le lettere dell'alfabeto
inglese, nelle due ultime parti saranno rappresentati ulteriori caratteri speciali e di interpunzione. La codifica
scelta è stata studiata con particolare riguardo ai problemi di conversione. Ad esempio, per passare da
ASCII a BCD, basta mascherare i 3 bit più significativi; per trasformare una stringa composta da lettere
minuscole in una stringa in lettere maiuscole, occorre semplicemente operare sul bit b5: forzando b5 = 0 ho
il carattere maiuscolo, forzando b5 = 1 ho il corrispondente carattere minuscolo).
In molti casi, ai sette bit del codice ASCII, viene aggiunto un ottavo bit, con significati diversi: per la
trasmissione di informazioni, l'ottavo bit è un bit ridondante, che trasforma il codice in h=2 e permette di
rilevare un errore su un singolo bit di un carattere. L'ottavo bit, in questo caso, viene chiamato CODICE DI
PARITÀ in quanto il suo valore è determinato in modo tale da rendere sempre pari (codici a parità pari
"PARITY EVEN") o sempre dispari (codici a parità dispari "PARITY ODD") il numero complessivo di bit "1".
Il codice, viene progettato dall’ECMA (European Computer Manufacturer's Association), ma adottato come
standard ISO solo nel 1987. Attualmente viene revisionato da un gruppo di lavoro costituito dalle
associazioni ISO/IEC/JTC1/SC2/WG3. Il codice, in realtà è costituito da una “famiglia” di codici che hanno in
comune il “code element” G0 da caricare nell’area GL, identico al codice ASCII. Ciascuna famiglia, quindi
definisce il relativo codice G1 da associare all’area GR e quindi definisce nell’insieme un codice ad 8 bit.
Attualmente il registro ISO 2375 contiene le seguenti registrazioni principali e le relative funzioni di controllo
per lo “shift-lock”
:
•ISO-IR 6 (ESC 02/08 04/02) : International Reference Version of ISO/IEC 646:1991
24
•ISO-IR 100 (ESC gg 04/01) : Latin Alphabet No.1, Supplementary Set (GR area of ISO 8859-1)
•ISO-IR 101 (ESC gg 04/02) : Latin Alphabet No.2, Supplementary Set (GR area of ISO 8859-2)
•ISO-IR 109 (ESC gg 04/03) : Latin Alphabet No.3, Supplementary Set (GR area of ISO 8859-3)
•ISO-IR 110 (ESC gg 04/04) : Latin Alphabet No.4, Supplementary Set (GR area of ISO 8859-4)
•ISO-IR 148 (ESC gg 04/13) : Latin Alphabet No.5, Supplementary Set (GR area of ISO/IEC 8859-9)
•ISO-IR 144 (ESC gg 04/12) : Cyrillic Supplementary Set (GR area of ISO/IEC 8859-5)
•ISO-IR 127 (ESC gg 04/07) : Arabic Supplementary Set (GR area of ISO 8859- 6)
•ISO-IR 126 (ESC gg 04/06) : Greek Supplementary Set (GR area of ISO 8859- 7)
•ISO-IR 138 (ESC gg 04/08) : Hebrew Supplementary Set (GR area of ISO 8859- 8)
•ISO-IR 154 (ESC gg 05/00) : Supplementary Set for Latin Alphabets No.1 or No.5, and No.2
•ISO-IR 155 (ESC gg 05/01) : Basic Box Drawing Set
•ISO-IR 156 (ESC gg 05/02) : Supplementary Set of ISO/IEC 6937
2.4.6 UNICODE
Mentre l’ISO 8859 è ancora in fase di evoluzione e sviluppo, non essendo ancora completo, l’adozione di un
set di caratteri a 16 bit risolve definitivamente tutti i problemi di comunicazione ed ambiguità. In effetti
UNICODE può essere visto come SUPERSET di tutti i linguaggi definiti.
Ogni singolo set di caratteri viene individuato dal range di codici usati, nella forma U+xxxx to U+yyyy dove
xxxx è il valore esadecimale di inizio del codice e yyyy è il valore esadecimale dell’ultimo carattere del
codice.
Per compatibilità il primo set di caratteri (C0 Control and Basic Latin) da U+0000 a U+007F è ancora la
tabella ascii a 7 bit. Il secondo set di caratteri (U+0080 to U+00FF) è costituito dall’insieme dei caratteri C1
Control and Latin-1 Supplement.
L’elenco completo dei set di caratteri attualmente definito è il seguente:
26
3. Il livello MV0 – Macchina fisica (Hardware)
3.1 MV0.0 – cenni sui circuiti analogici
Le seguenti note sono da considerarsi come un richiamo di nozioni di elettromagnetismo applicato ai circuiti
detti a "costanti concentrate" ovvero dove è possibile isolare nei singoli componenti di base i fenomeni
elettromagnetici che stanno alla base del funzionamento dei circuiti.
Lo scopo è di fornire una spiegazione, per quanto possibile elementare, ai fenomeni fisici che determinano il
funzionamento dei circuiti elettronici digitali e quindi dei processori.
La trattazione sarà quindi volutamente NON RIGOROSA in quanto tale trattazione si ritiene che debba
essere rimandata alle materie specifiche.
E’ possibile simulare il funzionamento dei circuiti segueti, usando questo simulatore:
https://www.falstad.com/circuit/
Al di là delle formule, la legge di Ohm stabilisce che, maggiore è la resistenza del conduttore, minore sarà la
corrente che vi circola, a parità di tensione applicata. Tale legge è confermata dai comportamenti che
normalmente seguiamo nella vita normale: se vogliamo evitare di prenderci la scossa, toccando un filo
scoperto a 220 Volt, usiamo scarpe di gomma che realizzano un circuito ad alta resistenza tra il filo ed il
potenziale di riferimento (terra) e quindi permette il passaggio di una corrente limitata. Viceversa, per le
cantine e per i luoghi umidi è consigliato un impianto a 12 o 24 Volt in quanto, in caso di contatto
accidentale, la corrente che fluirebbe nel nostro corpo sarebbe 20 o 10 volte inferiore rispetto alla normale
tensione di rete fornita dall'ENEL.
Conoscere la corrente che circola in un circuito ovvero in un percorso chiuso o maglia, consente di
conoscere la tensione ai capi di ogni singola resistenza che compone la maglia.
Nell'esempio a fianco, vediamo una maglia costituita dal solito
1
27
3.1.2 Condensatori
Un condensatore è un componente elettronico costituito da due armature separate da dielettrico. Applicando
un campo elettrico tra le armature, si accumula, progressivamente, della carica di segno opposto sulle
stesse, fino ad arrivare al valore di equilibrio dipendente dal valore di capacità C e dalla tensione applicata
V, secondo la formula q = V * C. Nel transitorio di carica, quindi possiamo immaginare che tale componente
si comporta come un resistore con valore di resistenza che varia da 0 (condensatore scarico) a infinito
(condensatore carico). Viceversa, quanto il condensatore è carico, può essere considerato come un
generatore di tensione variabile da V a 0 secondo un transitorio di scarica dipendente dal carico applicato.
Per analizzare i fenomeni di carica e scarica elettrica, usiamo i seguenti circuiti:
Circuito di carica
Se consideriamo il condensatore
R scarico, la sua resistenza varierà da
Vc
1
t
della corrente assorbita durante la
fase di carica: I = (V-Vc)/R RC
Il valore RC viene chiamato Costante di Tempo Caratteristica e ed indica il punto in cui la rapidità di
crescita pendenza della curva diminuisce.
Circuito di scarica
Se consideriamo il condensatore carico,
applicando la resistenza R la tensione
varierà da Vc a 0 secondo la seguente Vc
formuila esponenziale diagramata a
-t/RC
R C destra: Vc = V * e )
Applicando la legge di Ohm possiamo
ricavare anche il valore della corrente 37%
restituita durante la fase di scarica:
I = Vc/R
RC t
Il valore RC ovvero la Costante di Tempo Caratteristica, indica anche qui il punto in cui la rapidità di
scarica dimunuisce.
3.1.3 Induttori
Un induttore è un componente elettronico costituito da un filo conduttore avvolto a spira in aria oppure
intorno ad un nucleo ad alta permeabilità magnetica. Applicando un generatore di tensione ai capi del
conduttore, si accumula, progressivamente, energia, sotto forma di campo magnetico, fino ad arrivare al
valore di equilibrio dipendente dal valore di induttanza L e dalla corrente di equilibrio I, secondo la formula E
2
= L * I /2. Nel transitorio di carica, quindi possiamo immaginare che tale componente complementare ad un
condensatore ovvero si comporta come un resistore con valore di resistenza che varia da infinito (induttore
scarico) a 0 (induttore carico). Viceversa, quanto l’induttore è carico, può essere considerato come un
generatore di corrente variabile da I a 0 secondo un transitorio di scarica dipendente dal carico applicato.
Per analizzare i fenomeni di carica e scarica, usiamo i seguenti circuiti:
Circuito di carica
28
Se consideriamo l’induttore scarico, la
sua resistenza varierà da infinito a 0 e I
quindi la corrente ai capi dell’induttore
varia da 0 a I secondo la seguente
L 63%
formuila esponenziale diagramata a
-tR/L
destra: IL = I * (1 - e )
Applicando la legge di Ohm possiamo
ricavare anche il valore della tensione ai
capi dell’induttore durante la fase di
carica: V = V- R*IL
L/ t
R
Il valore L/R viene chiamato Costante di Tempo Caratteristica e ed indica il punto in cui la rapidità di
crescita pendenza della curva diminuisce
Circuito di scarica
Se consideriamo l’induttore carico,
applicando una resistenza ai capi. Si
comporterà come un generatore di Vc
corrente variabile da da I a 0 secondo la
seguente formuila esponenziale
-tR/L
diagramata a destra: IL = I * e
Applicando la legge di Ohm possiamo 37%
ricavare anche il valore della tensione ai
capi dell’induttore durante la fase di
scarica: V = R * IL RC t
Il valore L/R ovvero la Costante di Tempo Caratteristica, indica anche qui il punto in cui la rapidità di
scarica dimunuisce.
29
3.2 MV0.1 – Circuiti digitali e porte logiche
Si definisce circuito logico elementare o porta logica o semplicemente porta (gate) ogni circuito
elettronico ad n ingressi ed un’uscita che assume il valore 1 in corrispondenza delle configurazioni degli
ingressi descritte dalle funzioni AND, OR, NOT, NAND, NOR.
Per rappresentare le porte logiche, si utilizzano i seguenti simboli grafici:
Spesso, per connettere le uscite di più circuiti logici, si utilizzano le porte three-state ovvero delle porte che
possono presentare in uscita, oltre gli stati 0 e 1, anche lo stato di alta impedenza, corrispondente allo stato
di un circuito aperto. Il simbolo, per una porta three-state è il seguente:
dove il terminale x indica l’ingresso, il terminale y l’uscita ed il terminale s, l’abilitazione dell’uscita, secondo
la seguente tabella di verità:
s x y
0 0 circuito aperto
0 1 circuito aperto
1 0 0
1 1 1
Combinando opportunamente le porte logiche è possibile costruire i circuiti combinatori che realizzano una
determinata funzione Y = f(X).
Ad esempio, volendo realizzare un Half Adder, dovremmo realizzare un circuito a due ingressi (le due
variabili binarie) e due uscite (somma e riporto), secondo la seguente tabella di verità:
30
x1 x2 S C Per ognuna delle uscite, y1 (S) e y2 (C)
0 0 0 0 le rispettive funzioni che realizzano la
0 1 1 0 tabella della verità sono l'XOR e l'AND.
1 0 1 0 Il Circuito che realizza un half adder
1 1 0 1 sarà quindi quello a fianco.
Si lascia allo studente l'esercizio di ricavare lo schema di un full-adder ovvero di un addizionatore che tenga
presente anche dell'eventuale riporto precedente. Il circuito risultante avrà così 3 ingressi (x 1, x2 e CP) ed
ancora 2 uscite (S e C).
31
3.2.2 Esempi di applicazione dell’Algebra Booleana ai circuiti
I seguenti due circuiti sono equivalenti applicando la proprietà distribuitiva
32
3.2.3 Operatori Universali NAND e NOR
Dal teorema di De Morgan, si deduce che gli operatori AND, OR e NOT sono sovrabbondanti in quanto una
stessa espressione può essere realizzata solo con AND e NOT o solo con OR e NOT. Viceversa, i soli
operatori AND e OR, sono insufficienti per rappresentare una qualsiasi espressione logica. Per questo, si
sono introdotti i seguenti operatori:
NAND = negazione del prodotto, si indica col simbolo / . Esempio: x / y yx x y
NOR = negazione della somma, si indica col simbolo . Esempio: x y y x x y
Questi sono anche detti operatori universali in quanto ciascuno di essi può realizzare sia la negazione che la
somma od il prodotto:
( negazione) x/xx xxx
( s om ma ) x / y x y x y ( prodotto) x y x y x y
Somma:
Prodotto:
33
3.2.4 Operatore OR esclusivo XOR
x1 x2 x1 x2
0 0 0
0 1 1
1 0 1
1 1 0
x1 x2 x1 x2
0 0 1
0 1 0
1 0 0
1 1 1
34
3.3 Funzioni Logiche
Una funzione logica (o booleana o di commutazione) y = f(x) è una funzione in cui il dominio ed il codominio
sono costituiti dall’insieme (0,1), da cui si deduce che anche il numero di funzioni yi di una variabile è finito e
vale 4:
x y1 y2 y3 y4
0 0 0 1 1
1 0 1 0 1
Estendendo la definizione per più variabili, avremo che una funzione logica a n variabili y = f(x1, x2,..., xn ) è
n
una funzione il cui dominio è costituito da tutte e sole le n-ple (x1, x2,..., xn ) di elementi dell’insieme {0,1} ed
il cui codominio è l’insieme {0, 1} . Anche qui il numero di funzioni definibili sarà finito e pari al numero di
n
n
disposizioni semplici dei due elementi (0,1) su 2 posti e quindi pari a 22 .
Introducendo il vettore X che ha come componenti i valori delle variabili x1, x2,..., xn, ovvero X = y = (x1, x2,...,
xn ), possiamo interpretare X come il valore corrispondente al numero binario a n bit che si ottiene
accostando i simboli x1, x2,..., xn . Potremo quindi scrivere y = f(X).
Per rappresentare efficacemente la funzione y = f(X ) possiamo scrivere una tabella che riporti, per ogni
valore di X il corrispondente valore della y. Tale tabella viene detta Tabella di Verità.
Ad esempio (1.2.1):
X x1 x2 x3 y
0 0 0 0 1
1 0 0 1 0
2 0 1 0 0
3 0 1 1 1
4 1 0 0 0
5 1 0 1 1
6 1 1 0 1
7 1 1 1 0
Si definisce mintermine delle n variabili x1, x2,..., xn, ogni prodotto tra tutte le combinazioni di variabili xi,
dirette o negate. Ciascun mintermine mi si riferisce alla combinazione tale che, sostituendo 1 alle variabili
dirette e 0 alle variabili negate, si ottiene il valore decimale i. Nella tabella dell’esempio precedente, avremo i
seguenti mintermini:
m0 x1 x 2 x 3 m1 x1 x 2 x 3 m2 x1 x 2 x 3 m3 x1 x 2 x 3 ....
Possiamo ora rappresentare una funzione logica nella forma canonica di somma di prodotti utilizzando i
mintermini corrispondenti al valore 1 di y come addendi.
Ad esempio, la funzione dell’esempio precedente può essere scritta come:
y m0 m3 m5 m6 x1 x 2 x 3 x1 x 2 x 3 x1 x 2 x 3 x1 x 2 x 3 0,3,5,6
Per il principio di dualità, è possibile esprimere una stessa funzione logica anche nella forma canonica di
prodotto di somme. Definendo infatti il maxtermine come la somma di tutte le variabili x1, x2,..., xn dirette o
negate. Ciascun maxtermine Mi si riferirà ora alla combinazione tale che, sostituendo 0 alle variabili dirette e
1 alle variabili negate, si ottiene il valore decimale i. Nella tabella dell’esempio precedente, avremo i seguenti
maxtermini:
M 0 x1 x 2 x3 M1 x1 x 2 x3 M 2 x1 x 2 x3 M 3 x1 x 2 x3 ....
Per cui la funzione dell’esempio precedente potrà essere scritta come il prodotto dei maxtermini
corrispondenti al valore 0 di y:
y M1 M 2 M 4 M 7 x1 x 2 x 3 x1 x 2 x 3 x1 x 2 x 3 x1 x 2 x 3 1,2,4,7
35
3.3.1 Mappe di Karnaugh
Le mappe di Karnaugh sono delle tabelle che permettono di determinare graficamente i mintermini adiacenti
e quindi semplificare automaticamente l’espressione analitica che rappresenta la funzione logica.
E’ possibile usare, in maniera semplice, le mappe di Karnaugh per funzioni fino a 4 variabili, secondo i
seguenti schemi:
x1 x1 x2
x2 0 1 x3 00 01 11 10
__ _ _ __ _ _ _ __
0 x1x2 0 x1x2x3 x1x2x3 x1x2x3 x1x2x3
x1x2
_ __ _ _
1 x1x2 x1x2 1 x1x2x3 x1x2x3 x1x2x3 x1x2x3
x1 x2
x3 x4
00 01 11 10
____ _ __ _ _ ___
00 x1x2x3x4 x1x2x3x4 x1x2x3x4 x1x2x3x4
___ _ _ _ __
01 x1x2x3x4 x1x2x3x4 x1x2x3x4 x1x2x3x4
__ _ _
11 x1x2x3x4 x1x2x3x4 x1x2x3x4 x1x2x3x4
__ _ _ _ _ _ _
10 x1x2x3x4 x1x2x3x4 x1x2x3x4 x1x2x3x4
In pratica, a partire dalla tabella di verità o dalla forma canonica in somma di prodotti, si costruisce la mappa
di Karnaugh, scrivendo 1 in corrispondenza delle combinazioni che determinano y = f(X) = 1. I relativi gruppi
di 8, 4 o 2 1 contigui, possono essere espressi da un singolo prodotto, eliminando la o le variabili che
compaiono sia in forma diretta che negata, ovvero quelle relative alle colonne o righe con entrambi i valori 1
e 0.
36
3.3.1.1 Esempi di sviluppi in mappe di Karnaugh
X x1 x2 x3 y
0 0 0 0 1 x1 x2
1 0 0 1 0 x3 00 01 11 10
2 0 1 0 0
3 0 1 1 1
4 1 0 0 0 0 1
5 1 0 1 1
6 1 1 0 0
7 1 1 1 1 1 1 1 1
Avremo due possibili zone che portano ad una semplificazione della forma canonica. Scegliendo di
raggruppare le colonne 01 e 11, avremo il termine x 2.x3 scegliendo le colonne 11 e 10, avremo il termine
x1.x3 ; in definitiva, potremo semplificare immediatamente la forma canonica:
y x1x 2 x 3 x1 x 2 x 3 x1 x2 x3 x1 x2 x3
in:
y x1x 2 x 3 x 2 x 3 x1x 3
37
Seguono altre possibili situazioni dove si possono semplificare i termini tramite le mappe di Karnaugh
x1x2
x3x4x1x2 Out
00 01 11 10
0000 1
x3x4
0001 1
0010 1 00 1 1 1
0011 0
0100 0 01 1 1
0101 1
0110 0 11 1 1
0111 1
1000 0
10 1
1001 0
1010 1
1011 0 y x1x3 x 4 x1x 2 x 4 x2 x 4
1100 0
1101 1
1110 0
1111 1
x1 x2
Input Out
00 01 11 10
0000 1 x3 x4
0001 0
00 1 1 1
0010 1
0011 1
01 1
0100 0
0101 0
11 1 1 1 1
0110 0
0111 1
10 1 1 1
1000 1
1001 0
1010 1
y x3 x 4 x1 x 2 x2 x 4
1011 1
1100 1
1101 1
1110 1
1111 1
38
3.3.2 Da somme di prodotti a prodotti di somme
Le forme semplificate ottenute tramite le mappe di Karnaugh sono espressioni del tipo somma di prodotti;
volendo trovare l’analoga espressione sotto forma di prodotto di somme, basta applicare il teorema di De
Morgan alla funzione di uscita negata. Per questo occorre:
* prendere gli 0 della funzione y
* ottenere l’espressione come Somma di Prodotti
* Applicare De Morgan, ottenendo l’espressione in Prodotto di somme.
Esempio (1.3.4) :
y 0,1,2,3,5,7,8,9,10,11 forma canonica in somma di prodotti
x1 x2
00 01 11 10
x 3 x4
(0) (4) (12) (8)
00 1 1
Utilizzando le tecniche di sintesi viste nel paragrafo precedente, si potrebbe progettare e realizzare qualsiasi
circuito combinatorio.
Nella pratica, poiché le realizzazioni manifestano delle problematiche spesso comuni, si preferisce usare dei
circuiti MSI a terminali multipli che eseguono funzioni specifiche.
Analizziamone alcuni dei più usati:
I decodificatori sono circuiti multiterminali con numero di uscite pari al numero di parole del codice
realizzato dalle variabili di ingresso. In pratica ogni uscita assume il valore 1 in corrispondenza di una ed una
n
sola parola del codice. In generale, se il decodificatore ha n ingressi ed esistono 2 parole di codice, su
n
ognuna delle 2 uscite viene realizzato un mintermine delle n variabili di ingresso.
39
Esempio: 74139; dual 2 to 4 line decoder (decodificatore binario-decimale)
m
I codificatori sono circuiti con 2 ingressi (quante sono le parole di un codice a m bit) e m uscite. Il
funzionamento di tali circuiti è esattamente l'opposto di quello dei decodificatori.
40
3.4.2 Circuiti multiplexer e demultiplexer
Spesso nella progettazione di circuiti digitali occorre instradare su un circuito un segnale scelto tra n segnali
disponibili in ingresso e, viceversa, dover instradare su uno di m circuiti disponibili un determinato segnale di
ingresso.
Circuito
Generico
MULTI DEMUL
TIPLEXE
PLEXER R
k
I multiplexer sono circuiti che hanno 2 segnali in ingresso, k ingressi di controllo ed un'uscita y. Tale uscita
riporta il j-esimo ingresso, dove j è il valore dei k ingressi di controllo.
41
I demultiplexer sono circuiti che funzionano in modo
complementare ai multiplexer. Con riferimento alla figura a
destra, il singolo segnale in ingresso (IN) viene instradato, in
n
base al valore dell'ingresso di controllo (A, B) ad una delle 2 (4)
uscite (OUT0, OUT1, OUT2, OUT3).
42
3.5 Composizione di circuiti combinatori Standard
43
ALU elementare da 1 bit
Come applicazione concreta di quanto
appreso finora, mostriamo un esempio
di ALU in grado di eseguire le seguenti
operazioni elementari, selezionabili
tramite gli ingrassi:
F0 e F1 che selezionano una delle
seguenti operazioni base:
0 0 Full Adder : A + B + c
0 1 NOT B
1 0 A or B
1 1 A and B
Le operazioni base possono essere
“modificate” tramite i seguenti ingressi
ausiliari:
ENA abilita/disabilita operatore A
ENB abilita/disabilita operatore A
INVA inverte bit dell’operatore A
ALU a N bit
Componendo N ALU da 1 bit, si può
realizzare una ALU da N bit, seguendo
una logica di “concretizzzazione” per
livelli successivi, Da rilevare il ritardo
introdotto dai riporti “seriali”
44
3.6 Circuiti Sequenziali
Vengono chiamati sequenziali i circuiti che forniscono uscite dipendenti sia dai valori degli ingessi in
quell'istante, sia dai valori assunti dagli ingessi negli istanti precedenti, ovvero dalla storia degli ingressi.
L'uscita, vista come vettore, potrà essere scritta come:
Y = f(X, t)
Tale modello risulta poco utile in quanto:
- La presenza della variabile "tempo" impedisce una trattazione analitica dei circuiti in quanto l'algebra
booleana è per sua natura atemporale.
- Non viene evidenziata la dipendenza dell'uscita dalla "memoria" del circuito ovvero la dipendenza dalla
storia degli ingressi.
Y = fy(X, S)
Dove la scomparsa della variabile indipendente tempo è solamente fittizia in quanto tale dipendenza è
mascherata nell'espressione che determina lo stato S che sarà quindi funzione del tempo. Per completare
quindi la definizione di circuito sequenziale, occorrerà definire la funzione che determina lo stato successivo:
S' = fs(X, S)
a) Circuiti sequenziali sincroni : un segnale a frequenza costante (clock) determina l'istante successivo e
quindi governa la variazione dello stato.
b) Circuiti sequenziali asincroni : l'istante successivo è stabilito dalle costanti del circuito stesso (ritardo
di propagazione) e quindi il circuito funziona senza alcun segnale di sincronismo.
I circuiti utili per i sistemi di elaborazione sono proprio i circuiti sequenziali sincroni che possono essere
quindi essere rappresentati dal seguente modello strutturale:
Y
X Circuito
Combinatori
S S'
o
MEMOR
IA
CLOCK
45
X S'
fs X S'
S
fs
S
M
M
Y
fy fy Y
Y f y ( X , S ) Y f y ( S )
Modello di Mealy: Modello di Moore:
S ' f s ( X , S ) S ' f s ( X , S )
SIMBOLO S R Q
0 0 Non varia
Q
S
_ 0 1 0
1 0 1
R Q
1 1 non ammessa
1 1 Non ammessa
Con questo circuito si risolve il problema del cock impulsivo da fornire al singolo flip-flop SRT, rendendo il
circuito complessivo più solido ed indipendente dai diversi tempi di risposta delle porte logiche. Durante tutto
il periodo di tempo in cui il segnale C rimane a 1, sono abilitati gli ingressi S e R del primo SRT, secondo la
tabella vista precedentemente, ma l'uscita Q non cambia perché il secondo SRT è bloccato da C=0. Al
momento che C va basso, vengono disabilitati gli ingressi S e R, quindi rimangono stabili i valori S e R
trasferiti alle uscite Q/Q che, dopo un tempo Δt, vengono trasferiti verso l'uscita. Se C rimane basso, l’uscita
non può cambiare perché non cambiano i valori S e R intermedi. Al momento che C torna a 1, vengono
prelevati di nuovi gli ingressi che però non riescono a raggiugere l’uscita perché si presume che Δt sia
comunque inferiore al ritardo introdotto dalle porte AND+NOR del primo SRT.
47
Costituisce praticamente l'elemento base per le
memorie binarie e quindi anche per rappresentare 0/0
1/1 1/1
gli stati di una rete sequenziale.
Per questo è conveniente l’utilizzo del diagramma Q=0 Q=1
degli stati a fianco. Ad ogni transizione di stato 0/0
(variazione del clock) viene associata la coppia
Ingresso/Uscita ovvero D/Q
3.7.5 Flip-flop JK
Si comportano come gli SRT dove J=R e K=S. L'unica differenza consiste nel fatto di accettare anche la
configurazione J=K=1 in corrispondenza della quale l'uscita Q cambia di stato (se era 0 diventa 1 e
viceversa). La funzione logica è la D = JQ' + K'Q
Un J-K può essere realizzato a partire da un flip-flop di tipo D secondo lo schema seguente:
SIMBOLO C J K Qn+1
0 X X Qn
Q
J 0 0 Qn
C 1 0 1
_
0 1 0
K Q 1 1 Qn (Toggle)
Utilizzi tipici:
Divisore/contatore asincrono (ripple counter) Shift Register
A B A B
1 Q 1 Q
J J S Q Q
C _ C _ J J
C _ C _
Q Q
K K Q Q
K K
Applicando il clock su C, avremo una frequenza Ad ogni impulso di clock il dato presente in
dimezzata su A e 1/4 su B e così via. BA è anche il ingresso viene trasferito allo stadio successivo (A)
contatore binario degli impulsi su C. e così via di seguito fino all'uscita.
48
3.8 Sintesi dei circuiti Sequenziali
Dopo aver fatto l’analisi della macchina sequenziale tramite il diagramma degli stati, si determina la tabella di
verità per le funzioni di uscita e di stato e quindi, usando una combinazione di FF tra quelli base
(Normalmente il tipo D o JK) si costruisce il circuito.
Per comprendere meglio il procedimento da seguire vediamo un esempio:
1/1 S1 S1 S2 0 001
La transizione degli stati sarà come indicato nella tabella a fianco. S2 S2 S3 0 010
S3 S3 S4 0 011
Usando FF JK avremo la seguente tabella della verità, per passare
da Sn a Sn+1: S4 S4 S0 1 100
q2q1q0 J2 K2 J1 K1 J0 K0 M5
000 0 x 0 x 1 x 0
001 0 x 1 x x 1 0
010 0 x x 0 1 x 0
011 1 x x 1 x 1 0
100 x 1 0 x 0 x 1
Applicando direttamente il metodo di Karnaugh per le uscite E quindi il seguente circuito:
otteniamo:
J2 = q2\q1q0 00 01 11 10 K2= q2\q1q0 00 01 11 10
q1q0 0 1 1 0 x x x x
1 x x x x 1 1 x x x
M5= q2
49
3.8.2 Progetto di un contatore sincrono modulo N con FF tipo D
Scegliendo di usare dei FF tipo D, l’analisi e la sintesi è la seguente:
D D D M
q2q1q0 2 1 0 5
000 0 0 1 0
001 0 1 0 0
010 0 1 1 0
011 1 0 0 0
100 0 0 0 1
D1 q2\q1q0 00 01 11 10
q1'q0+q1q0’ 0 1 1
1 x x x
D0 q2\q1q0 00 01 11 10
q2'q0' 0 1 1
1 x x x
50
3.9 Elementi di progettazione dei computer – stima dei costi
L’evoluzione della Macchina di Von Neumann procede con un ritmo straordinario che richiede agli
imprenditori del settore una visione lungimirante o, meglio, “paranoica” al fine di valorizzando i consistenti
investimenti richiesti e superare l’alta volatilità causata dal breve ciclo di vita. In questo capitolo, verranno
messi in evidenza alcuni principi euristici derivati dall’analisi storica e da parametri propri della tecnologia di
base dell’Hardware.
Nella figura a destra, in particolare, si può individuare il “cuore” di un Circuito Integrato che è costituito da
una sottile piastrina di semiconduttore (tipicamente silicio) sulla quale vengono realizzati, con opportune
tecniche di “drogaggio” e di fotolitografia i componenti attivi (transistor) e passivi (resistori e condensatori).
La piastrina di silicio, per poter essere collegata ad altri componenti deve essere impacchettata in un
contenitore protettivo di materiale plastico dal quale fuoriescono i terminali (piedini) corrispondenti ai segnali
di ingresso/uscita.
Vediamo ora brevemente le fasi principali il processo produttivo di un Circuito Integrato (chip):
51
4) I wafer vengono quindi tagliati per separare i diversi “chip”: quello buoni
verranno impacchettati in contenitori standard per poter essere impiegati nei
circuiti elettronici, quelli difettosi verranno riciclati o usati come gadgets.
Da queste semplici indicazioni possiamo ricavare il costo di un singolo Circuito integrato dalla formula:
costo del chip = [(costo del die) + (costo del test) + (costo impacchettamento)]/(resa test finale)
dove:
(costo del die) = (costo del wafer)/((numero die per wafer) * (resa del die))
(numero die per wafer) = (area wafer) / (area die) - (numero die periferici)
I die periferici sono quelli che si trovano sulla circonferenza del wafer e quindi sono incompleti, da scartare.
Per calcolarne quanti ne sono, possiamo usare questa formula approssimata:
Esempio:
Si abbia un wafer di 20 cm di diametro in cui ricavare die da 1.5 * 1.5 cm; quanti dies è possibile ricavare?
Soluzione:
(area wafer) = * 10 * 10 = 314,1593 cmq
(area die) ? 1.5 * 1.5 = 2.25 cmq
(circonferenza del wafer) = * 20 = 62,83185 cm
(diagonale del die) = . ) 2 (15
(15 . ) 2 = 2,12132 cm
quindi:
(numero die periferici) = 62,83185 / 2,12132 = 29,62
( numero die per wafer) = 314,1593 / 2.25 - 29,62 = 110
Se, nell’esempio precedente, la misura del die fosse stata di 1x1 cm, avremmo ottenuto:
Per calcolare il numero di dies ottenibile, occorre ora considerare che ciascun wafer contiene un certo
numero di difetti casuali per cui la resa dovrà essere calcolata come:
dove (resa del wafer) è la probabilità che il wafer non sia completamente inutilizzabile. Assumiamo per
semplicità questa probabilità =1 ovvero che non sia da buttare, allora rimangono da calcolare gli altri fattori.
La (densità difetti) dipende da quanto è maturo il processo di fabbricazione e può variare dal 0.6 a 1.2 difetti
/cmq. Alfa dipende dalla complessità del processo di fabbricazione del chip ovvero dal numero di livelli di
incisione e, per gli attuali CMOS vale circa 3.
Quindi, proseguendo l’esempio precedente, assumendo la resa del wafer al 100%, la densità dei difetti di
0.8 /cmq e alfa = 3, avremo i seguenti valori di resa del die nei due casi:
1) die da 1.5x1.5 cm : (1 + 0,8 * 2,25 / 3)^ (-3) = 0,244 (meno di un quarto sono buoni)
2) die da 1x1 cm: (1 + 0,8 / 3) ^ (-3) = 0,492 (meno della metà sono buoni)
52
1) die da 1.5x1.5 cm : ($3500) / (27) = $130
2) die da 1x1 cm: ($3500) / (132) = $27
quindi possiamo vedere che, raddoppiando l’area di un die, il costo di produzione si quadruplica. In
generale, quindi possiamo concludere che l’esigenza di miniaturizzazione ha una forte giustificazione nel
risparmio che è possibile ottenere da essa: più grande è il die, moltissimo di più sarà il costo.
53
4. Il livello MV1 – Struttura dei calcolatori (Firmware)
MEMORIA PROCESSORE
Anche se tale schema potrebbe sembrare “banale”, esso introduce degli importanti concetti, che ancor oggi
sono validi, che razionalizzano l’analisi e la progettazione. Il computer è costituito da due componenti base: il
processore e la memoria che sviluppano il seguente processo
Il processore estrae le istruzioni dalla memoria, le interpreta e le esegue fino alla risoluzione del problema.
I dati, vengono trasformati dal programma in esecuzione, attraverso una successione di stati, fino al
raggiungimento dello stato finale, nel quale i dati sono i risultati ottenuti.
In ogni caso è evidente che il modello è effettivamente esaustivo, comprende quanto occorre per il
funzionamento del computer e mette in evidenza che l'elemento base della macchina di Von Neumann è la
sua PROCEDURALITÀ ovvero la necessità di dover descrivere, alla macchina, il problema da risolvere,
secondo una sequenza di operazioni elementari o istruzioni.
Il semplice modello di Von Neumann può essere arricchito e completato con gli altri elementi accessori che
fanno parte dei moderni computer, come ad esempio nella figura seguente:
I/O
Central
Memori Hard Stampan Tastiera
Processing
a Disk te
Unit
Central
e
BUS
Il modello aggiunge agli elementi Processore (o CPU) e Memoria, le unità di Input/Output che possono
essere:
- solo di Input, es.: la Tastiera
54
- solo di output, es.: la stampante
- sia di Input che di Output, es.: Hard Disk
ma non cambia, nella sostanza, nessuno dei concetti fondamentali evidenziati dalla figura 3.1
A questo livello, possiamo astrarre in senso orizzontale e quindi individuare le seguenti Unità Fondamentali:
Il BUS rappresenta il mezzo trasmissivo tramite il quale è possibile stabilire i collegamenti fra le varie unità. Il
progetto del BUS, in termini di numero di connessioni e di protocollo per le trasmissioni, caratterizza
decisamente il funzionamento del sistema complessivo. Non a caso, nel linguaggio comune, si tende ad
associare l'architettura del sistema con il progetto del BUS (es: Architetture I.S.A., E.I.S.A., M.C.A., ....)
Le UNITÀ FUNZIONALI e di I/O sono costituite da particolari moduli dedicati ad assolvere funzioni
specifiche, e di cooperazione con le attività svolte dalla RETE di GOVERNO. Le Unità funzionali possono
essere: Multiple, Indipendenti, Ripetute, Asincrone tra loro, Opzionali.
La Rete di Governo può essere paragonata al "capoufficio" del sistema: il suo compito è quello di controllo e
coordinamento delle attività svolte dalle unità funzionali. La R.G. svolge il suo compito secondo il seguente
CICLO COMPLETO:
Nei sistemi attuali, si preferisce inserire la fase di aggiornamento del PC immediatamente dopo la fase di
FETCH, in modo da poter effettuare agevolmente il "PREFETCHING".
Per comprendere meglio il funzionamento delle varie fasi, introduciamo oltre al Program Counter e gli
operandi (locazioni di memoria o registri) – tipici della Macchina di Von Neumann, anche i seguenti registri
generici ma indispensabili per il livello MV1:
M.A.R.: Memory Address Register: buffer afferente alle linee di indirizzo della memoria. Per
indirizzare una locazione di memoria, occorre caricare opportunamente questo registro.
M.D.R.: Memory Data Register: Buffer afferente alle linee dati della memoria. Leggendo o
scrivendo su questo registro si legge o si scrive la locazione di memoria indirizzata dal
MAR.
I.R. : Instruction Register: Buffer di parcheggio per l'istruzione correntemente in elaborazione.
P.S.W.: Processor Status Word: insieme di flag che condizionano l’esecuzione delle istruzioni
(principalmente artmetico-logiche)
Il Ciclo della rete di governo può ora essere illustrato dal seguente diagramma di flusso:
55
FETCH: (estrazione dell'istruzione)
Questa fase può essere sintetizzata dalle seguenti operazioni:
PC -> MAR, read (linea R/W a 1)
MDR -> IR
UPDATEPC: aggiornamento Program Counter
PC + n -> PC (n= lunghezza dell'istruzione)
DECODE: (analisi dello IR)
Questa è una fase che prende tempo, in quanto occorre
predispor¬re gli opportuni switch, affinché la singola istruzione
sequenziale possa essere eseguita. Durante questo tempo, non si
ha accesso al BUS esterno che quindi potrebbe essere utilizzato
da altre unità.
SEARCH-OPERAND: (caricamento operandi)
Questa fase può essere opzionale (tempo esecuzuone nullo), nel
caso in cui l'istruzione sia a zero operandi; in tutti gli altri casi,
occorre effettuare di nuovo un accesso alla memoria, secondo una
tecnica analoga a quella vista per la fase di FETCH.
EXECUTE: (esecuzione)
È questa la fase “operativa” fondamentale ed in genere viene
devoluta ad una delle Unità Funzionali. In questa fase, è
ricompresa l'eventuale memorizzazione dei risultati con una fase
di "write-operand".
Al termine dell’esecuzione, vengono aggiornate le flags della PSW
e quindi, poiché il PC è stato aggiornato subito dopo la fase di
FETCH, possiamo dire che l'istruzione è effettivamente terminata
e si può iniziare ad eseguire la prossima istruzione.
4.1.2 Le Interruzioni
Il ciclo precedentemente esposto, rappresenta però semplice-
mente una MACCHINA di TURING, incapace di reagire in
maniera asincrona agli eventi esterni. Per questo, sono stati
indicati i punti (a) e (b) nei quali il ciclo può essere interrotto per
uno dei possibili eventi:
Per giudicare il grado di confidenza Real Time si utilizzano i seguenti parametri significativi:
- tr: Tempo di rilevamento (detection time) = tempo necessario affinché la R.G. rilevi la presenza
dell'interruzione in corso
- ts: Tempo di servizio = tempo necessario per eseguire il segmento di programma associato all'evento.
(questo tempo dipende dalle istruzioni che devono essere eseguite)
56
- tsmin: Tempo minimo di servizio = Tempo necessario per eseguire una routine di interruzione NULLA
(Return from Interrupt)
Per massimizzare le prestazioni, è necessario minimizzare il tempo di servizio: per questo si può agire sui
parametri:
tr: per minimizzare tr, occorre evitare l'utilizzo di istruzioni che possono durare un tempo non
determinabile a-priori. Ovviamente, le istruzioni eseguite quando sono disabilitati gli interrupt,
sono da considerarsi come una singola istruzione e quindi considerati nel calcolo di tr.
ts-tr: varia da macchina a macchina e dal tipo di interrupt dalle decine alle centinaia di cicli di clock.
Per minimizzare questo tempo, occorre cercare di fare il minor numero di operazioni possibili e
farle utilizzando tecniche ottimizzate.
tsmin-tr: Dipende da come viene effettuata la COMMUTAZIONE di CONTESTO. Dopo aver rilevato il
segnale di interruzione, la RG deve salvare (nello stack) il contesto del programma in esecuzione
e caricare il contesto della routine di servizio. Al termine della routine di servizio, la RG dovrà
ripristinare il contesto del programma interrotto, prima di proseguire con l'esecuzione.
- A VETTORE DIRETTO:
La R.G. presenta un certo numero di linee dedicate agli interrupt. Quando una di queste linee viene
attivata, si può immediatamente riconoscere l'evento e quindi l'indirizzo in memoria della routine associata.
Questo tipo di interrupt, permette di ottenere tempi minimi di servizio molto bassi (applicazioni militari e Real
Time in senso stretto).
- VETTORIZZATO A CODICE:
La RG, ha un'unica linea sulla quale vengono segnalati gli interrupt. Per determinare l'evento associato e
quindi la locazione di memoria della routine di servizio, occorre effettuare un accesso a MDR e leggere
quindi il codice presente; questo codice viene utilizzato come indice per accedere ad una tabella in memoria
che conterrà l'indirizzo effettivo della routine di servizio.
57
Ma la classificazione degli interrupt può anche essere eseguita in base alla natura temporale dell’evento:
- SINCRONO
Il verificarsi dell’evento è sincronizzato con il clock della CPU, ovvero con il codice da eseguire. In
questa categoria vengono inclusi gli interrupt classificati in base al trattamento o natura:
TRAP (trappole)
Si riferiscono ad interruzioni nel normale programma ed esecuzione delle primitive della
Macchina virtuale di riferimento. Si utilizza la caratteristica di salvataggio di contesto per
semplificare o rendere più affidabile il richiamo di tali primitive. Al ritorno da una TRAP si continua
con l’istruzione successiva
FAULT (Errore)
Sono eccezioni dovute ad errori nella normale esecuzione del programma. In genere il FAULT
può essere recuperabile (es,. Pagina non trovata) per cui, al ritorno dal un FAULT si riesegue
l’istruzione che aveva causato il FAULT.
- ASINCRONO
L’evento si verifica scorrelato dal clock. Fanno parte di questa categoria anche le interruzioni dell’orologi
di sistema, purché questo non sia fisicamente connesso con il clock del processore.
All’inizio si sta eseguendo un programma “utente” (priorità=0), quando, al tempo t=10, la stampante genera
58
un interrupt e quindi avendo una priorità = 2, superiore al quella in esecuzione, viene avviata la relativa
routine di servizio associata mentre il programma utente rimane in attesa, con il relativo contesto
memorizzato nello stack. Durante l’esecuzione di tale routine, al tempo t=15, arriva un’interruzione da parte
della linea seriale (RS232) e, poiché tale dispositivo ha una priorità = 5 , viene avviata la relativa routine di
servizio mentre rimane in attesa la routine della stampante, con il relativo contesto memorizzato nello stack.
Al tempo t = 20, arriva un’interruzione da parte del disco ma, avendo priorità = 4, viene ignorata tale
interruzione che quindi rimane “pendente”.
Al tempo t = 25, la routine di servizio della RS232 termina e quindi la CPU si predispone a continuare
l’esecuzione della routine di servizio della stampante che però ha priorità = 2 minore dell’interruzione
“pendente” e pertanto, prima di poter eseguire una sola istruzione, viene avviata ed eseguita la routine di
servizio associata al disco che aveva priorità = 4. Al termine di tale routine (t = 35) viene effettivamente
ripresa la routine della stampante che termina al tempo t = 40 dove, finalmente, si può riprendere il
programma utente con priorità = 0.
Dispo
sitivi
di I/O
Figura 4.6 - Modello gerarchico della Memoria di un elaboratore
La forma triangolare della figura trasmette sinteticamente le specifiche e le caratteristiche dei diversi livelli
gerarchici di memoria: verso il vertice del triangolo diminuisce la capacità ma aumenta il valore strategico
ovvero la velocità di funzionamento, la vicinanza con il processore e, inevitabilmente, il costo.
Nell’esempio sopra, troviamo come prima tipologia di memoria, i Registri macchina, il tipo di memoria che è
incluso nel Data Path e quindi devono avere una velocità di accesso tale da supportare il ciclo della rete di
governo ovvero con la frequenza di clock del processore.
Scendendo di livello troviamo subito dopo la cache e quindi la memoria centrale chiamata anche “memoria di
lavoro” che ha delle caratteristiche di velocità e capacità tali da consentire il buon funzionamento dei
processi di elaborazione.
Scendendo ancora di livello, troviamo i dischi magnetici, dei dispositivi di I/O usati tipicamente come
memoria di archiviazione di lungo termine ma anche come appoggio per la memoria centrale nel caso
questa non sia di dimensioni sufficienti per tutti i processi.
Alla base del modello, ultima per velocità, ma di dimensioni grandi a piacere, troviamo la memoria utilizzata
tipicamente per i backup ovvero per le copie “fuori linea” su supporti magnetici o ottici.
Analizziamo ora in dettaglio le caratteristiche delle principali tipologie di memoria centrale.
La memoria a dischi magnetici e la memoria a dischi ottici la tratteremo come dispositivi di I/O in quanto
hanno entrambe le caratteristiche di “memoria” e dispositivo di I/O.
In particolare:
1) I dischi magnetici, occupando il primo livello superiore, nel modello gerarchico, tra le memorie
59
esterne, ci lascia intuire che è possibile utilizzare tale tipologia di memoria come
supporto/estensione della memoria centrale.
Tale tipo di memoria, mantiene infatti una delle caratteristiche principali della memoria della Macchina di Von
Neumann la possibilità di accedere direttamente ad una locazione specificandone il suo indirizzo.
Rispetto alla RAM, tale tipo di memoria, contro una minor velocità di accesso offre:
- la possibilità di mantenere le informazioni nel tempo anche se si spegne l’elaboratore
- la possibilità di avere grandi capacità di memoria a basso costo
2) I nastri magnetici ed i dischi ottici sono alla base della piramide della memoria mentre, a metà
strada, non evidenziati nello schema, si collocano i dischi “magneto-ottici”: dischi che utilizzano una
formattazione analoga a quella dei dischi ottici ma sfrutta i campi elettromagnetici per cambiare le
proprietà di riflessione di alcuni materiali. La loro diffusione è stata bassa per cui ne tralasciamo
l’analisi approfondita, visto anche questa similarità di funzionamento con i dischi magnetici ed ottici.
I nastri magnetici ed i dischi ottici consentono di memorizzare grandi volumi di dati a basso costo
ma, in genere, non è possibile disporne direttamente, così come previsto dalla macchina di Von
Neumann. Per poter leggere un particolare insieme di dati, si esegue prima una procedura di
“restore” che trasferisce quell’insieme di dati su un’area del disco magnetico.
Allo stesso modo, per memorizzare su tali dispositivi, si preparano le informazioni su un’area del
disco magnetico e quindi si esegue una funzione di backup.
Questo non significa che, viceversa, che non sia possibile l’accesso al dispositivo senza passare su
disco magnetico; tale accesso è possibile e consentito ma deve sottostare ad un vincolo specifico e
limitante: quello di essere una memoria prettamente sequenziale.
60
4.3 La Memoria di lavoro
Lo schema seguente classifica funzionalmente le diverse tipologie di memoria di lavoro:
Per ognuna di queste, poi si hanno le ulteriori classificazioni, come dettagliato nel seguito:
61
4.3.3 ROM
Le memorie ROM sono utilizzate per registrare le costanti ovvero parametri definibili “una tantum” e da qui la
caratteristica “Read Only”. Nella pratica tali memorie sono anche “riscrivibili” ma tale operazione va
considerata come eccezionale o sporadica, necessaria per l’inizializzazione del sistema o per la sua
riconfigurazione. Questa caratteristica rende tale memoria ideale per contenere programmi predefiniti e
protetti da possibili manomissioni (virus informatici).
Tra le ROM bipolari possiamo distinguere:
Mask ROM sono ROM costruite in fabbrica usando una maschera apposita in una delle fasi di
impianto fotolitografico. Tali ROM sono altamente costose e dovrebbero essere riservate per
memorizzare informazioni o programmi consolidati e destinate a dispositivi che garantiscono un
sufficiente volume di produzione.
PROM (Programmable ROM) sono molto più versatili in quanto possono essere programmate
dall’utente, una sola volta. Possiamo immaginare tali memorie come costituite da matrici di “fusibili”
che escono dalla produzione di fabbrica tutti “intatti”: per memorizzare “0” è sufficiente, usando un
apposito circuito elettrico “programmatore”, “bruciare” il fusibile mentre per memorizzare “1”, si lascia
il fusibile intatto.
EPROM - Erasable Programmable Read Only Memory, sono particolari PROM che possono
essere cancellate esponendo il circuito ai raggi Ultravioletti (UV) di lunghezza d’onda di 235 nm per
un tempo di almeno 20 minuti. Per questo i chip sono dotati di una finestrella trasparente attraverso
la quale è visibile il chip. Il bombardamento di fotoni sul chip provoca l’eliminazione delle cariche sui
gate floating riportando il dispositivo allo stato “vergine”.
EAROM - Electrically Alterable Read Only Memory, chiamata anche EEPROM, Electrically
Erasable Programmable Read Only Memory, sono una variante delle EPROM che, grazie
all’utilizzo di uno strato di ossido di gate più sottile possono essere cancellate semplicemente
applicando un campo elettrico inverso a quello di programmazione e generato internamente al
dispositivo stesso (tramite circuiti chiamati “charge pump”). In tale categoria di ROM, con una piccola
variante che ne diminuisce le dimensioni, da cui il termine “Flash Memory”, rientrano le ormai
comuni USB-PEN capaci di memorizzare oltre 8 GB di memoria. Per tali dispositivi si utilizza uno
schema “seriale” che consente di accedere al chip utilizzando, al massimo, 8 pin.
62
4.4 Dispositivi di Input e Output
Per dispositivi di I/O si intendono delle unità funzionali particolari che connettono il mondo esterno con il
computer.
Nel paragrafo precedente abbiamo ricompreso, tra i dispositivi di I/O anche i dischi magnetici, i dischi ottici
ed altri dispositivi utilizzati per l’archiviazione “permanente” ovvero capaci di registrare e dati dalla memoria
centrale e, viceversa, caricare i dati dall’esterno. Questa caratteristica fa rientrare tale “memoria”, a pieno,
titolo tra i dispositivi di I/O.
Tenuto cono dell’eterogeneità dei dispositivi, è quindi necessario analizzarne gli aspetti di basso livello,
esulando quindi dalle caratteristiche “visibili” (Video, tastiera, stampante, nastro, CD, ecc.).
Sotto questo aspetto, qualsiasi I/O può essere visto come un “flusso di dati” (Stream) in ingresso o in uscita.
Per controllare tale flusso, sono possono essere classificati secondo tre principali schemi:
1) Flusso controllato da programma (I/O programmato).
2) Flusso governato da “eventi” (“interrupt” – vedi par. 6.2)
3) Flusso con accesso diretto in memoria (DMA)
Nei casi più generali, una periferica viene gestita tramite due registri, uno per il flusso di Input/Output ed un
altro per controllo del flusso:
- Registro dati, dove leggere il dato in ingresso o scrivere il dato in uscita.
- Registro di stato che fornisce informazioni sulla disponibilità e qualità del dato ricevuto oppure indica se
è possibile e trasmettere un dato in uscita e quindi procedere alla trasmissione.
Ad esempio, per trasmettere una stringa di caratteri ascii alla stampante, occorre aspettare che la stampante
sia “ready” ovvero sia accesa, connessa, “on line” e pronta a ricevere un carattere alla volta ovvero abbia
terminato la stampa del carattere precedentemente inviato. Se supponiamo che lo stato di “Ready” sia
definita dal bit 7 del “PrinterStatusRegister”, l’algoritmo da eseguire è del tipo:
Un esempio di input è la lettura di un carattere ascii proveniente dalla tastiera di un terminale, supponendo
che la presenza di tale carattere venga segnalata dal bit 0 del “TerminalStatusRegister”. Il codice da
eseguire è del tipo:
63
Lo schema a controllo di programma, se da una parte è molto semplice da implementare, dall’altra ha
importanti difetti:
1) In caso di trasmissione, nel caso venga spenta la stampante, il programma rimane bloccato, senza
possibilità di svolgere altre elaborazioni, finché non la si riaccende;
2) In caso di ricezione, il programma rimane bloccato finché un utente non si decide a trasmettere.
Nel caso tali difetti non siamo compatibili con le funzionalità da sviluppare, occorre utilizzare uno dei due
schemi, ad Interrupt o in DMA.
64
4.4.1 Accesso diretto alla Memoria (DMA)
A completamento delle problematiche di interrupt e di I/O, accenniamo brevemente alla tecnica di D.M.A.
(direct memory access). Questa tecnica, viene avviata come una interruzione: la CPU, in questo caso, non
deve eseguire alcuna routine, ma deve solo concedere l'accesso alla memoria alla periferica che necessita
I/O.
- DMA FISICO:
è la tecnica adottata nei mainframe e presuppone l'esistenza dei CANALI BUFFERIZZATI DI I/O secondo il
seguente schema:
Su ogni canale possono essere collegati più di un controller I/O e ogni controller può avere collegati più
periferiche e più canali di I/O.
La memoria dovrà essere di tipo "dual port" su base prioritaria (priorità alla periferica in quanto più lenta).
L'istruzione di I/O dovrà supportare un indirizzo così strutturato:
da inserire in opportune "MAIL BOX" che verranno lette e dal controller di I/O di destinazione. Il controller
effettuerà il trasferimento dei dati in maniera autonoma ed asincrona rispetto alla RG.
- DMA LOGICO:
Nei sistemi a uP, per abbassare il costo della periferia, si preferisce utilizzare lo stesso bus utilizzato dalla
RG, per la lettura/scrittura in memoria. Il protocollo da seguire è il seguente:
PERIFERICA R.G.
Bus Request
Bus Grant
Trasferimento (bus non collegato)
DMA DONE
Ripresa del BUS
La gestione della priorità può essere su base posizionale secondo lo schema Daisy-Chain:
65
Esempio di un Sistema in DMA per PC.
Nei primi PC, il DMA veniva usato
utilizzando un componente intermedio,
chiamato “DMA Controller”, che gestiva I
registry necessari, liberando le periferiche
dall’implementazione di sistemi
complessi.
Tale schema fu presto abbandonato
perché non portava grossi vantaggi
rispetto al semplice schema di Interrupt, in
presenza di un unico BUS. Durnate il
trasferimento, nfatti, la CPU può solo
attendere mentre potrebbe eseguire del
codice. In questi casi si parla di “cycle
stealing” (Furto di cicli di RG)
Un miglioramento delle prestazioni si po’ ottenere trasferendo il controllore di DMA nel dispositivo di I/O o
comunque all’esterno del sistema BUS-CPU, come nella figura seguente:
Un risultato ancora migliore si può ottenere dedicando all’I/O un bus specializzato, separato dal sistema
BUS-CPU (System bus) come nello schema seguente:
Quest’ultimo schema ha molti elementi in comune con l’attuale organizzazione “multibus” di moderni PC,
come riportato nel seguente schema:
66
Tale schema risponde alla strategia di compatibilità che, finora, ha decretato il successo dei moderni PC
basati su INTEL. L’architettura “multibus” prevede che il bus di sistema, quello che connette la memoria alla
CPU (o meglio alla CACHE), viene collegato al BUS PCI tramite un “Bridge” ovvero un componente
hardware (un microchip) che trasforma i protocolli del bus della CPU nel protocollo PCI e viceversa. IL PCI
BUS può essere quindi considerato il BUS principale sul quale si connette, utilizzando l’opportuno BRIDGE il
BUS ISA.
Nello schema, notiamo la presenza di un ulteriore BUS, il BUS SCSI, che viene connesso tramite un
“controller” piuttosto che tramite un “bridge”. La differenza tra le due modalità è intuibile: nel caso di
BRIDGE, la differenza di protocolli tra i due bus è trasparente al software: Il driver per la “Sound Card” vede
a tutti gli effetti una periferica connessa al bus ISA e la gestisce come se vi fosse una connessione diretta al
BUS ISA.
Nel caso invece del bus SCSI, il corrispondente driver software accede alle periferiche SCSI, inviando gli
opportuni comandi al Controller SCSI che quindi viene vista come una periferica specifica connessa al BUS
PCI.
Nel sistema vi sono altri BUS, tutti connessi tramite opportuni controller, ne citiamo alcuni:
- BUS IDE (ATA) per connettere fino a due dischi IDE (fino a 133 MB/s)
- BUS SATA (fino a 150 MB/s)
- Gigabit Ethernet (fino a 125 MB/s)
- IEE 1394B – Firewire (fino a 100 MB/s)
- USB v 2.0 (fino a 48 MB/s)
67
4.4.2 Canali e processori di I/O
Nella tecnica del DMA abbiamo sottolineato che alleggerire la CPU delle attività di I/O, non sempre porta ad
un vantaggio concreto, soprattutto se il “controller DMA” accede alla memoria in maniera meno efficiente
della CPU.
Un’evoluzione importante avviene con l'introduzione del concetto di modulo I/O capace di eseguire unop
spezzone di programma dedicato al trasferimento in o da memoria centrale. Canali di I/O e Processori di I/O
sono terminologie che stanno ad indicare un’evoluzione del DMA, nella direzione di eseguire le attività senza
disturbare l’esecuzione delle istruzioni da parte della CPU.
La CPU, quindi, non esegue le istruzioni di I/O ma fornisce al canale di I/O solo le informazioni sul
programma, presente in memoria centrale. Tale programma (una sequenza di Channel Command Word –
CCW) viene quindi eseguito dal canale indirizzando i dispositivi indicati e trasferendo i dati da o per la
Memoria di lavoro.
Canale Multiplexor:
In questo caso il canale dispone di più BUS, ad
ognuno dei quali può essere connesso uno o più
controllori di I/O.
Questo consente di gestire direttamente più
dispositivi nello stesso istante e disaccoppiare le
diverse prestazioni di ciascun dispositivo ovvero
evitare che dispositivi più lenti possano ritardare
dispositivi in grado di trasferire dati ad alta velocità.
La tecnica dei canali e processori di I/O risale ai primi IBM 360 e possiamo trovare una
qualche analogia nel canale “SCSI” visto precedentemente.
68
4.5 La Microprogrammazione
La microprogrammazione è una tecnica particolare che permette di superare, almeno parzialmente, i grossi
problemi di obsolescenza tecnologica nei progetti dei computer. Attualmente si assiste ad un tasso di
obsolescenza, per quanto concerne le tecniche costruttive, di circa 3 anni: sotto queste condizioni, una
realizzazione che comporti un tempo di sviluppo superiore ad un anno risulta essere fortemente
antieconomica.
Nelle macchine microprogrammate, la Rete di governo è costituita ancora da una macchina di Von Neumann
costituita da:
Definito il progetto architetturale generale, si può aumentare la longevità di una macchina semplicemente
potenziando la rete di governo in termini di aggiunta di moduli di elaborazione e di microcodice.
Possiamo distinguere i seguenti orientamenti:
- Microprogrammazione MISTA
è la più diffusa: può essere vista come una microprogrammazione orizzontale dove, però, i bit meno
significativi indicano l'indirizzo della prossima microistruzione da eseguire.
69
5. Il livello MV2: La macchina “Assembler”
La Macchina virtuale accessibile a livello MV2 è quella che classicamente viene trattata, parlando delle
Macchine di Von Neumann. Nel nostro approccio, abbiamo preferito distinguere i due livelli per mascherare,
a questo livello, le problematiche inerenti le prestazioni della Rete di Governo. A questo livello di macchina
virtuale, le Risorse disponibili (R2) sono le locazioni di memoria ed Registri generali e di I/O; il Linguaggio
disponibile (L2) viene chiamato “Linguaggio Macchina” o “Assembler”.
Viene anche referenziata come interfaccia tra software e hardware in quanto storicamente era così che
funzionavano i primi computer, spesso programmati direttamente in linguaggio macchina al fine aumentare
la velocità di esecuzione e ridurre al minimo l’occupazione di memoria (vedi esempio).
Nell'analisi di un'architettura di un calcolatore al livello MV2, il "Programming Model" consiste in una scheda
sintetica che descrive il modello di riferimento da usare per programmare in “assembler” la macchina a livello
MV2.
Le informazioni contenute indicano in genere:
Nella pratica, la schematizzazione precedente non è sempre così netta per cui le definizioni sono spesso
trasversali. Nel seguito, si preferisce adottare il consueto approccio metodologico, definendo prima le
Risorse (R2) e poi il linguaggio (L2).
70
5.1.1.1 I Registri
I registri rappresentano lo stato del processore ovvero il “contesto” del programma in esecuzione in quanto
memorizzano i risultati temporanei delle operazioni artimetico-logiche. In generale sono dispositivi a 2^n
stati, dove n indica la larghezza della parola identificativa della macchina (es.: macchina a 8 bit o 16 bit).
Alcuni registri, comunque di livello MV2, possono essere resi inaccessibili ovvero protetti in base al livello di
esecuzione in corso (es.: livello “kernel”).
Forniscono un supporto di rappresentazione per la maggior parte dei tipi di dati supportati dalla macchina.
Le modalità di impiego dei registri, distinguono due classi di macchine:
AD ACCUMULATORE, dove esistono registri specializzati per compiere particolari operazioni.
A REGISTRI GENERALI, dove tutti i registri sono equivalenti.
Nella pratica, la distinzione non è così netta: ad esempio, possono esserci macchine con più registri
“generali” e diversi registri specializzati per cui, per poter fare una classificazione dovremmo valutare
l’utilizzo “prevalente” di tali registri.
Un caso particolare di macchina “ad accumulatore” è la “Java Virtual Machine” che utilizza il “top dello stack”
come registri operandi. Secondo una semantica “RPN” (Notazione Polacca Inversa).
Nel caso vi siano registri di diversa larghezza, per identificare la macchina si farà riferimento
all’accumulatore ovvero ai registri generali equivalenti.
Alcuni registri sono visibili, sia a livello MV1 che a livello MV2. Ad esempio, lo Stack Pointer può essere
usato da MV1 per eseguire le istruzioni di CALL o di servizio alle interruzioni ma anche da MV2 per
memorizzare ulteriori valori nello stack.
Un importante registro condiviso è il Processor Status Word (PSW o flags) che contiene generalmente le
seguenti flag binarie:
N: 1 quanto il risultato è negativo
Z: 1 quando il risultato è zero
V: 1 quando il risultato è oVerflow
C: 1 quando il risultato ha dato un bit di riporto (Carry)
A: 1 quando c’è riporto Ausiliario (bit 3, per BCD)
P : 1 quando il risultato ha una parità pari
5.1.1.2 La Memoria
La memoria, chiamata più precisamente “memoria di lavoro”, è un insieme di p dispositivi a 2^n stati (cella
elementare), dove p indica la capacità della memoria espressa in PAROLE e n è la larghezza della
PAROLA. In genere la larghezza della parola di memoria coincide con la larghezza dei registri
La memoria fornisce soltanto un SUPPORTO di rappresentazione dei vari tipi di dati anche se, dal set di
istruzioni del processore, potrebbe apparire che le operazioni avvengano direttamente in memoria. Come
caso limite, possiamo distinguere le architetture "MEMORIA-MEMORIA", dove i registri della macchina sono
completamente trasparenti al programmatore e tutte le istruzioni fanno riferimento esclusivamente a
locazioni di memoria.
La memoria, così come i REGISTRI sono dei dispositivi ad ACCESSO CASUALE, ovvero strutturati in
modo tale che l'accesso ad una determinata informazione è indipendente dalla collocazione fisica della
stessa.
Una volta definita la cella elementare di memoria, occorre stabilire come raggruppare le parole che hanno
lunghezza multipla della cella elementare. Oltre che scegliere la strategia little o big endian, occorre anche
stabilire se occorre “allineare” gli indirizzi alle parole multiple oppure no, come da figura seguente:
71
Nel modello di Von Neumann, ad ogni locazione di memoria viene associato un indirizzo univoco ma non si
stabilisce se possono esistere più aree di memoria “specializzate”, ad esempio, per contenere codici di
programma.
Per aumentare le prestazioni della CPU, è diventata una scelta consueta (JVM, AVR, …) quella di separare
l’area di memoria contente i dai dall’area di memoria contente le istruzioni. Tale architettura viene
chiamata “Harvard” per ricordare il primo computer elettromeccanico sviluppato da Howard Aiken per conto
della IBM, chiamato “Mark I” ed utilizzato dall’Università di Harvard. Il computer leggeva le istruzioni da un
nastro perforato di carta che era fisicamente distinto dalla “memoria” costituita da contatori elettromeccanici.
Possono essere definite più di di aree di memoria, dipendentemente da esigenze specifiche: per ciascuna di
esse potrebbe essere necessario definire semantiche di accesso distinte, come ad esempio:
a) accesso sequenziale (sincrono), ovvero è necessario accedere alla locazione seguendo
rigorosamente un protocollo sequenziale prestabilito (address MAR, wait for ready, MDR
Register);
b) accesso asincrono, si immette l’indirizzo senza aspettare l’effettiva disponibilità (address MAR).
E’ una semantica analoga al DMA, svincola la velocità di elaborazione dalla velocità della memoria
ma necessita di controlli di “dipendenza” tra i dati che obbligherebbero a dover forzare la
sincronizzazione.
L’accesso alla memoria potrebbe prevedere due ulteriori possibili modelli:
a) Accesso lineare: occorre fornire l’indirizzo lineare di dimensione sufficiente a coprire l’intera
dimensione della memoria (es.: se abbiamo 1 Gbyte di memoria occorrono indirizzi da almeno 30
bit)
b) Accesso segmentato: l’indirizzo è composto da due parti: Segmento, che individua la parte alta di un
indirizzo completo e Offset all’interno del segmento. In generale, il segmento viene impostato in un
registro della macchina e quindi i riferimenti successivi possono essere effettuati usando solo l’offset
e quindi indirizzi più corti. Tale modello offre il notevole vantaggi della rilocabilità immediata del
codice di programma ed è quindi alla base della multiprogrammazione/virtualizzione.
Per individuare questi sottoinsiemi, si potrebbe far riferimento al formato di ISTRUZIONE che, normalmente
non è fisso, così come la lunghezza di ciascuna istruzione che potrebbe essere “fissa” (casi a e b) oppure
variabile (caso c)
Per mettere un po’ di ordine nel “variegato” contesto illustrato sopra, possiamo provare a scomporre il
formato di una istruzione in modo da identificare una parte che rimane “fissa”, in base alla quale poter
identificare la sottoclasse semantica di riferimento. Per far questo, è opportuno scomporre l’istruzione
secondo il seguente FORMATO GENERALE:
<CODICE OPERATIVO><MODIFICATORI><OPERANDI>
72
dove:
- CODICE OPERATIVO
è quella parte di lunghezza fissa individuabile in una posizione ben precisa della stringa di bit. Questo, non
rappresenta il codice dell'istruzione, bensì la sottoclasse omogenea.
- MODIFICATORI
Sono dei bit aggiuntivi che rendono sostanzialmente variabile il codice dell'istruzione vera e propria. I
modificatori determinano essenzialmente la lunghezza in byte ed in tempo dell'istruzione completa.
- OPERANDI
In genere possiamo avere da 0,1,2 o 3 operandi, secondo la classificazione: a) zero-operandi; b)
monoperandi (monadiche); c) bioperandi (diadiche) o d) trioperandi (triadiche).
Ogni operando può essere, o direttamente il valore da trattare o un indirizzo di un registro o di una locazione
di memoria che potrà contenere a sua volta, o il valore da trattare o ancora un indirizzo, e così via.
La modalità con la quale ci si può riferire ad un operando viene anche indicata “modalità di indirizzamento”,
così come illustrata nel paragrafo successivo.
OPERANDO ---->|INFORMAZIONE|
a3) N=infinito INDIRIZZAMENTO INDIRETTO ARBITRARIO: ogni parola della macchina utilizza un
particolare bit che indica se il riferimento è IMMEDIATO o indiretto: in questo modo è possibile definire
qualsiasi livello di indirizzamento. Ad esempio, supponiamo di avere una parola da 16 bit in cui il bit 15
indichi 0 = immediato, 1 = indiretto:
N.B.:
Se pensiamo ad un livello di indirezione N= -1, ritroviamo il caso dell'indirizzamento IMMEDIATO.
b) INDIRIZZAMENTO MODIFICATO
L'indirizzo viene determinato sommandolo ad una BASE; tipicamente si usa il contenuto di un REGISTRO
INDICE.
c) INDIRIZZAMENTO RELATIVO
L 'indirizzo viene determinato relativamente a qualche altro registro; tipicamente il riferimento è il Program
Counter. Questo metodo viene utilizzato per scrivere codice rilocabile (JUMP indipendenti dalla posizione del
programma in memoria) e per i salti "CORTI" ovvero dove lo "spiazzamento è di lunghezza inferiore a quella
del registro di riferimento.
I tre metodi fondamentali precedenti, possono essere combinati tra di loro per ottenere tutti i possibili altri
metodi elencati nel set di istruzioni dei processori. Bisogna fare bene attenzione a come questi metodi
vengono combinati, in quanto ci sono pericoli di ambiguità. Ad esempio nell'indirizzamento indiretto
modificato, bisogna sapere bene cosa bisogna sommare e a chi.
Per completezza, citiamo altri due metodi DERIVATI, frequentemente usati, che possono essere considerati
come casi particolari o combinazione dei precedenti e che prevedono:
a) Indirizzamento mediante REGISTRO - quando la locazione di memoria è in realtà un registro
b) Indirizzamento con AUTOINCREMENTO/AUTODECREMENTO
dove abbiamo indicato con “x” se esiste (ovvero ha senso) la particolare combinazione tra <opcode> e
<modificatore>. Nell’esempio sopra, le istruzioni MUL e DIV tra numeri decimali (BCD) non esiste e quindi
non può essere utilizzata (l’assemblatore ritorna errore).
La presenza di caselle “vuote”, non sempre è indice di una bassa “ortogonalità”: è necessario verificare che il
codice macchina relativo alla combinazione non ammessa, non venga effettivamente utilizzato.
Nell’esempio precedente, il codice macchina “001110…..” (MUL tra BCD) potrebbe essere assegnato ad
altra famiglia di istruzioni.
Se invece il codice non è utilizzato, allora potremmo essere di fronte a due situazioni:
a) Il progettista intende riservare l’implementazione di quell’istruzione nelle versioni future del
processore;
b) Si tratta di istruzioni “fantasma” ovvero delle istruzioni che non sono documentate ma sono
comunque implementate. Tale strategia, apparentemente incomprensibile potrebbe essere
giustificata da uno dei seguenti motivi:
I. tattica commerciale finalizzata ad allungare il ciclo di vita del processore: l’istruzione non è al
momento, richiesta dal mercato ma nel caso fosse richiesta, la si potrebbe semplicemente
documentare aggredendo il mercato in tempi rapidissimi;
II. verifica collaborativa: l’istruzione non è stata ancora verificata in tutte le possibili casistiche
ed applicazioni e pertanto se ne rilascia una documentazione riservata ai clienti “tester”;
75
5.1.5 Controllo flusso istruzioni
Le istruzioni di salto ovvero di controllo del flusso delle istruzioni da eseguire meritano un ulteriore
approfondimento in quanto sono alla base delle tecniche di programmazione.
L’importanza di tali istruzioni viene ben
rappresentata dal seguente grafico temporale
del Program Counter che, se non ci fossero
istruzioni di controllo, continuerebbe a
crescere secondo una funzione monotona
(grafico a).
Normalmente, ogni programma prevede di
interrompere il flusso in base a determinate
circostanze, provocando quindi
un’alterazione del flusso come indicato nel
grafico b).
Nello sviluppare le procedure, una caratteristica che potrebbe essere utile ed interessante curare, è quella
della “ricorsività” ovvero la possibilità che, all’interno della procedura, sia possibile richiamare
“consistentemente” sé stessa.
Una procedura può essere considerata “ricorsiva” qualora sia ben “incapsulata” ovvero il contesto (insieme
delle variabii “locali” utilizzate) abbia una visibilità “privata” ed esista fin tanto che esiste la procedura. La
comunicazione tra chiamante e procedura avviene esclusivamente tramite gli “argomenti” passati in ingresso
e gli argomenti di ritorno in uscita, così come avviene per le normali istruzioni macchina.
Vediamo un classico esempio di procedura ricorsiva nel prossimo paragrafo.
76
5.1.5.2 Le Coroutine
L’utilizzo delle procedure si basa su uno schema
“gerarchico” asimmetrico che distingue nettamente il
programma chiamante dal programma chiamato:
ogno volta che viene chiamata una procedura, si
riparte sempre dall’inizio (iniziando eventualmente
un nuovo contesto) mentre nel programma
chiamante si prosegue (e si mantiene il conteato
originale).
Piuò essere utile, in diverse situazioni, avere uno
schema simmetrico come illustrato nello schema a
fianco dove le due procedure A e B si comportano in
modo equivalente: quando A richiama B, B riprende
l’esecuzione dal punto dove si era interrotto
precedentemente (ovvero dall’inizio se non era stata
mai chiamata).
Quando B, intende chiamare la procedura A, questa
riparte dal punto in cui si era interrotta per richiamare
la procedura B, e così via.
Due procedure che si comportano in questo modo
vengono dette “coroutine”.
L’impiego ricorrente delle coroutine è nello
sviluppare programmi che possano essere eseguiri
in parallelo da più processori, ovvero simulando tale
esecuzione segmentandolo in più coroutine.
L’implementazione di tali meccanismo a livello MV2
potrebbe essere banale: basterebbe implementare
un’unica istruzione (RESUME), sia per la chiamata
che per il ritorno, che, una volta inizializzata
correttamente, effettua semplicemente lo scambio
tra (SP) e PC.
Raramente si trovano tali istruzioni e comunque sarebbero limitate all’utilizzo di due sole coroutine.
Normalmente quindi si preferisce implementare tale mecanismo ai livelli superiori (MV3/MV4) tramite
strutture dati persistenti che mantengano traccia dello stato del PC di ciascuna coroutine, oltre che del
relativo contesto.
77
Per illustrare efficacemente il
funzionamento di una conversione tra
notazione infissa ed una RPN, si
utilizza l’algoritmo di E.W. Dijstra:
l’espressione infissa viene analizzata
da sinistra verso destra, immaginando
che sia un convoglio ferroviario che va
da New York in California, con una
deviazione a metà verso il Texas.
Quando le carrozze arrivano alla
deviazione, se contengono una
variabile, proseguono direttamente
verso la California, altrimenti si
procede secondo la seguente tabella
decisionale basata sullo stato
precedente, ovvero in
all’ultima carrozza deviata verso il Texas, si seleziona la riga della
tabella e si esegua l’azione indicata nella colonna corrispondente
all’operatore contenuto nella carrozza in esame, azione codificata
nel seguente modo:
1) push carrozza su Texas
2) pop carrozza da Texas
3) delete carrozza da Texas e da NY
4) STOP: In California troveremo la stringa RPN, da eseguirsi da
Sinistra a Destra
5) STOP: Errore su livelli parentesi
Il simbolo ┴ indica l’inizio e la fine della stringa.
Eseguiamo quindi, passo passo, la conversione della stringa
indicata nella figura precedente, aggiornando gli stati California e
e Texas
Elaborando l’espressione: A*(B+C) otteniamo il seguente “trace”:
NY Azione California Texas
A Prosegue vero la California A ┴
x Riga ┴ Push Texas A x┴
( Riga x Push Texas A (x┴
B Prosegue vero la California AB (x┴
+ Riga ( Push Texas AB +(x┴
C Prosegue vero la California ABC +(x┴
) Riga + Pop Texas ABC+ (x┴
) Riga ( delete carrozza da Texas e da NY ABC+ x┴
┴ Riga x Pop Texas ABC+x ┴
┴ Riga ┴ STOP
Eseguendo l’espressione RPN seguiamo, nell’ordine, lo stato dello stack:
A A A A Ax(B+C)
B B C B + (B+C) x
C
78
5.1.5.4 Le procedure ricorsive – Gestione dell’heap
Affinché una procedura sia in grado di richiamare se stessa, questa deve poter utilizzare un contesto
indipendente da quello della procedura chiamante con la quale viene quindi condivisa solo la parte “codice”
(read-only).
La gestione del contesto è simile a quella vista al livello MV1 per la gestione degli interrupt, dove il contesto
era riferito all’insieme dei registri utilizzati dalla routine di servizio il quale, prima di iniziare l’esecuzione del
nuovo codice veniva salvato nello stack e quindi, al termine dell’esecuzione, veniva ripristinato.
Ai livelli superiori, occorre includere nel contesto anche le variabili di
lavoro e prevedere, per l’accesso a tali variabili una modalità di
indirizzamento indiretto di tipo “modificato e relativo”. Da un punto di
vista teorico, si potrebbe usare sempre SP come registro relativo che
contiene la base e quindi riferirsi alle variabili di lavoro tramite un offset
immediato, Tale scelta richiede molta attenzione in quanto SP ha un
utilizzo frequente e dinamico e quindi la base potrebbe variare e quindi
anche i relativi offset dovrebbero essere ricalcolati. Per ovviare a tale
problema viene usato un registro specifico, denominato “Frame
Pointer” (FP) o “Base Pointer” (BP).
L’insieme (chiamato genericamente “heap” – “mucchio”) dei registri e
delle variabili di lavoro (chiamate anche variabili ad allocazione
“automatica”) si posiziona nell’area di stack, allocata nella parte finale
della memoria di lavoro, come indicato nello schema a destra.
Lo schema evidenzia l’“heap” come la porzione di contesto che aggiunta, rispetto ai registri generali e che è
costituita: a) dall’insieme dei registri strutturati per la gestione delle procedure, b) l’insieme degli argomenti
passati dal chiamante; c) l’insieme delle variabili automatiche, referenziabili con indirizzo relativo a FP.
Con tale strutturazione, risulta evidente che, ogni qualvolta una procedura chiama sé stessa, in fase di
inizializzazione dovrà costruire una nuova area di “heap” nello stack e quindi iniziare ad eseguire il codice
dall’inizio, operando sul contesto appena costruito e dove lo SP della procedura verrà riposizionato in coda
al FP, salvaguardando quindi l’intero contesto della procedura chiamante.
Al termine della chiamata, sarà sufficiente ripristinare il puntatore all’heap originario, conservati nell’attuale
heap, e quindi riprendere l’esecuzione della procedura chiamante.
79
6. Architetture Parallele
Vengono definite parallele le architetture dei sistemi di elaborazione che prevedono una qualche forma di
parallelismo a livello MV1 ovvero dove sia possibile che due o più unità di elaborazione possano espletare le
rispettive funzionalità in modalità concorrente. Per questo, ovviamente occorre evitare che vi sia una
dipendenza temporale tra le rispettive unità di elaborazione ovvero occorrerà implementare degli opportuni
meccanismi di sincronizzazione e di arbitrio che consentano l'accesso alle risorse condivise.
6.1 Il Pipelining
La forma più elementare di parallelismo si ottiene prendendo lo spunto dal funzionamento delle catene di
montaggio di una fabbrica.
Occorrono parecchie ore per montare un'automobile; come può una fabbrica di automobili produrre
un’automobile ogni pochi minuti? Usando una catena di montaggio con molte stazioni: ogni stazione
aggiunge una parte o due al telaio in alcuni minuti e quindi trasmette il telaio alla stazione seguente.
Possono passare diverse ore prima che un telaio passi attraverso un intera catena di montaggio ma questo
non è importante: il rendimento della linea di produzione è una nuova automobile ogni pochi minuti.
Per ogni fase occorre uno o più cicli di clock. Come riescono le attuali CPU ad eseguire un'istruzione ogni
ciclo di clock?
Una strategia molto diffusa è usare una pipeline.
La pipeline è composta da un certo numero di fasi ed ogni fase effettua un'operazione sull’istruzione per ogni
80
ciclo di clock. Le istruzioni attraversano la pipeline così come avviene per le automobili in una catena di
montaggio.
Ogni fase della pipeline deve ultimare i relativi lavori in un ciclo di clock: in questo modo il periodo di clock è
determinato dalla fase più lunga. Per minimizzare il periodo di clock il lavoro dovrebbe essere equilibrato
così tutte le fasi completino le loro operazioni in tempi quasi uguali, adottando, ad esempio, i seguenti
accorgimenti:
• accorpare fasi che complessivamente rientrano nel tempo di clock
• usare memoria di lavoro veloce, anche grazie all’utilizzo della memoria “cache” e dei registri
Per fissare le idee, prendiamo come modello una delle istruzioni più lunghe, ovvero quella relativa al
caricamento di un valore dalla memoria con un indirizzamento indicizzato (esempio: Ry Mem[Rx+100]):
occorre eseguire le seguenti operazioni:
Le azioni 1 e 2 possono essere realizzate simultaneamente: il valore corrente del PC è usato per prelevare
l'istruzione dalla memoria mentre si sta calcolando il nuovo valore del PC;
Le azioni 3 e 4 possono anche essere realizzate simultaneamente: il registro indice può essere letto mentre
si sta decodificando il codice operativo.
in tempi quasi uguali a quelli di esecuzione: questo consente di usare una pipeline a sei-fasi secondo il
seguente schema:
A Six-Stage Pipeline
FI DI CO FO EI WO
Fetch Decodifica Calcolo indirizzo Fetch Operandi Esegue Scrive il risultato
dell'istruzione l'istruzione; effettivo degli Legge gli l’operazione dell’ALU o
dalla memoria Legge il valore operandi l’operandi in nell’ALU o calcola l’operando da
usando il PC dal registro memoria; l'indirizzo di memoria nel
come indirizzo; indice;. memoria effettivo; registro di
Incrementa il PC Test la condizione destinazione;
per puntare di branch;
all'istruzione
successiva;
Nota 1:
La fase di DI preleva sempre i valore da un registro indice senza considerare il codice operativo: in realtà
occorre prelevare, da una posizione fissa del codice operativo, l’identificatore del registro indice. Questo
comporta il caricamento inutile del registro indice nel caso l’istruzione non lo usi ma, se si aspettasse la
decodifica, si avrebbe un rallentamento qualora si dovesse caricare il valore da uno dei registri indice;
eventuali istruzioni che non hanno bisogno dei registri indice ignoreranno i valori caricati.
Il flusso delle istruzioni attraverso la pipeline può essere indicato con un diagramma temporale ovvero con la
81
seguente tabella dove appare evidente che, al tempo T6, si raggiunge l’obiettivo di poter eseguire
un’istruzione per ciclo di clock ovvero avere le Unità Funzionali dei 6 stadi pienamente operative
contemporaneamente:
Purtroppo, non è sempre possibile mantenere la pipeline “piena”. Ad esempio, se si sta eseguendo un jump
o un branch condizionato e questo risulta “vero”, il PC viene cambiato dalla fase di WO. Nel frattempo, però,
già le quattro istruzioni seguenti sono state prelevate e caricate nella pipeline. Per procedere è quindi
necessario svuotare la pipeline e ricominciare dalla prima istruzione puntata dal PC appena aggiornato,
come da schema seguente:
82
Questo è un tipico caso di “control-hazard” che verrà discusso in seguito.
Affinché ogni fase della pipeline possa operare indipendentemente dalle altre, è necessario immagazzinare
temporaneamente le istruzioni, gli operandi, i registri indice, ecc., in opportuni “buffer” o “latches” che hanno
due accessi, uno a sinistra, che conserva le informazioni necessarie alla fase a sinistra, ed uno a destra, che
conserva le informazioni necessarie alla fase di destra:
Ad esempio, tra lo stadio FI e DI avremo:
FI stage FI/DI DI stage
latch
Dove il latch dovrà contenere le seguenti informazioni:
NPC = prossimo valore del PC
IR = Istruzione in fase di decodifica
Tk = [k + (n-1)] T
dove:
k*T = tempo necessario per eseguire la prima istruzione e riempire la pipeline
83
(n-1)*T = tempo necessario per eseguire le istruzioni successive, supponendo che la pipeline
rimanga sempre piena
In realtà le cose vanno ancora peggio, in quanto non abbiamo tenuto conto di altri fattori, come il tempo
necessario per svuotare la pipeline, in caso di salti, e che, per alcune istruzioni, delle fasi possono essere
nulle.
Supponiamo che:
la fase FI richieda 10 nS,
la fase di DI richieda 8 nS,
la fase di CO richieda 6 nS
la fase di FO richieda 10 nS
la fase EI richieda 10 nS,
la fase di WO richieda 7 nS.
La fase più lunga richiede 10 nS; la pipeline aggiunge un ulteriore overhead di 1 nanosecondo a causa dei
latch della pipeline e dell'obliquità della rampa di clock.
Il periodo di clock sarà quindi di 11 nS ovvero la frequenza di clock sarà di 90,909 MHz. La pipeline eseguirà
le istruzioni ad una velocità massima di 90,909 MIPS (se CPI = 1 e n grandissimo).
Senza pipeline, il periodo di clock può essere ridotto a 10 nS (la frequenza di clock è quindi di 100 MHz).
L'istruzione successiva non viene prelevata prima che sia completa l'istruzione corrente. Ogni istruzione di
load da memoria richiede sei cicli di clock; supponiamo che le altre istruzioni richiedano soltanto quattro cicli
di clock.
84
Supponiamo inoltre che il 40% delle istruzioni siano load da memoria. Il CPI sarà quindi:
(40%) * (6 cicli) + (60%) * (4 ciclo) = 4,8 cicli/istruzione ovvero le istruzioni vengono eseguite ad una velocità
di (100) / (4,8) = 20,833 MIPS.
Se la situazione è la stessa per qualsiasi programma possiamo dire che la pipeline migliora la prestazione
della CPU di un fattore di (90,909 MIPS) / (20,833 MIPS) = 4,36
Un altro modo di procedere senza pipeline consiste nel prendere come riferimento il periodo di clock relativo
all'istruzione più lunga. Un'istruzione di load da memoria richiede 10 + 8 + 6 + 10 + 10 + 7 = 51 nS ovvero il
periodo di clock è di 51 nS e la frequenza di clock è di 19,61 MHz. Ogni istruzione è eseguita in un ciclo di
clock ovvero CPI = 1,0 le istruzioni vengono eseguite alla velocità di 19,61 MIPS. In questo caso la pipeline
migliorerà la prestazione della CPU da fattore di (90,909 MIPS) / (19,61 MIPS) = 4,64
In definitiva, l’uso del pipelining migliora le prestazioni del CPU da un fattore di 4,36 o di 4,64 a seconda
della strategia usata. Il calcolo effettuato, comunque, non tiene conto dei pipeline hazards per cui il
miglioramento reale delle prestazioni sarà comunque più basso.
85
6.1.2 Pipeline hazards (Conflitti potenziali nella pipeline)
1. Strutturali: (Structural-hazard) si presentano quando due o i più fasi della pipeline provano a realizzare
delle azioni in conflitto sulla stessa risorsa di hardware.
2. Sui dati: (Data-hazard) si possono presentare quando l'ordine temporale delle read/write su una variabile
è permutato.
3. Di controllo: (Control-hazard) si presentano quando le istruzioni modificano il normale incremento del
PC.
Un hazard può essere corretto bloccando l'istruzione critica nella fase della pipeline per uno o più cicli di
clock fino a che non esiste più conflitto e quindi è possibile trattare l'istruzione.
Supponiamo che un’istruzione i si blocca nella fase k della pipeline.
Il blocco viene effettuata lasciando immutato il latch della pipeline precedente la fase k; il latch conserva
l'istruzione i invece di cambiare all'istruzione i+1.
Le istruzioni che seguono l'istruzione i (i+1, i+2, ecc.) nelle fasi precedenti della pipeline (k-1, k-2, ecc.)
devono essere bloccate cosicché tutti i latches precedenti della pipeline (ed il PC) rimangono invariati.
Tutte le istruzioni seguenti sono bloccate piuttosto che continuare ad attraversare la pipeline.
Le istruzioni che precedono i (i-1, i-2, ecc.) nelle fasi successive della pipeline (k+1, k+2, ecc.) continuare
il loro normale flusso attraverso la pipeline.
In pratica si crea una discontinuità (una bolla) fra le istruzioni i-1 ed i. La bolla attraversa le fasi k+1, k+2,
ecc., della pipeline e non deve cambiare alcun registro, posizione di memoria, ecc. Il latch successivo la fase
k della pipeline viene quindi caricato con un'istruzione nop (No Operation) cosicché la bolla non fa nulla.
Come esempio di blocco, consideriamo il seguente frammento di codice su una pipeline a 5 stadi (Fase di DI
e CO, sono spesso aggregabili):
1. R6 Mem[R1+100]
2. R1 R2+R3
3. R4 R1-R5
4. R3 Mem[R2+10]
Questo codice presenta un data-hazard tra l’istruzione 2 e 3; l’istruzione 3 deve essere bloccata nella fase
DI fino a quando la fase WO ha finito di caricare il registro R1 con il risultato della somma. Possiamo
tracciare il seguente diagramma temporale:
FI DI FO EI WO
T1 R6Mem[R1+100]
T2 R1 R2+R3 R6Mem[R1+100
]
T3 R4 R1-R5 R1 R2+R3 R6Mem[R1+100
]
T4 R3 Mem[R2+10]* R4 R1-R5 (bloc.) R1 R2+R3 R6Mem[R1+100
]
T5 R3 Mem[R2+10]* R4 R1-R5 (bloc.) Bolla 1 R1 R2+R3 R6Mem[R1+100
]
T6 R3 Mem[R2+10]* R4 R1-R5 (bloc.) Bolla 2 Bolla 1 R1 R2+R3
T7 R3 Mem[R2+10] R4 R1-R5 Bolla 3 Bolla 2 Bolla 1
T8 R3 Mem[R2+10] R4 R1-R5 Bolla 3 Bolla 2
T9 R3 Mem[R2+10] R4 R1-R5 Bolla 3
T10 R3 Mem[R2+10] R4 R1-R5
T11 R3 Mem[R2+10]
Nell’esempio precedente il data-hazard crea 3 cicli di blocco (e 3 di bolla) ovvero le 4 istruzioni richiedono 7
cicli di clock anziché i 4 previsti. Il CPI per questa sequenza di codice a 4 istruzioni diventa 7 / 4 = 1,75
anziché 1.
Usando il calcolo del fattore di miglioramento usato precedentemente (e supponendo il tempo di DI,
comprensivo di CO = 10 nS) avremo quindi:
Il periodo di clock sarà sempre di 11 nS ovvero la frequenza di clock sarà di 90,909 MHz.
Se CPI = 1,0 la velocità è di 90,909 MIPS.
Ogni istruzione di load da memoria richiede cinque cicli di clock; supponiamo che le altre istruzioni
richiedano soltanto quattro cicli di clock.
Supponiamo inoltre che il 40% delle istruzioni siano load da memoria. Il CPI sarà quindi:
(40%) * (5 cicli) + (60%) * (4 ciclo) = 4,4 cicli/istruzione ovvero le istruzioni vengono eseguite ad una velocità
di (100) / (4,4) = 22,727 MIPS.
Se la situazione è la stessa per qualsiasi programma possiamo dire che la pipeline migliora la prestazione
della CPU di un fattore di (90,909 MIPS) / (22,427 MIPS) = 4,0
Con CPI = 1,75,l’aumento di performance della pipeline, così come calcolato precedentemente, scende
pesantemente durante l’esecuzione di queste 4 istruzioni da 4,0 a 4,0/1,75 = 2,28
(stesso risultato otteniamo se partiamo da una velocità abbattuta da 90,909 a 90,909/1,75 = 51,948 MIPS).
Il calcolo del fattore di miglioramento delle prestazioni dovuto alla pipeline, e quindi il CPI effettivo, dovrebbe
essere basato su grandi sequenze di codice nel seguente modo:
dove:
per cui:
Calcoliamo ad esempio il CPI nel caso che il 15% delle istruzioni di un lungo programma provochino un
hazard ed ogni hazard causi 3 cicli bloccati:
Conclusioni
Da questa breve trattazione possiamo concludere che, comunque, l’utilizzo del pipelining migliora le
prestazioni della singola CPU, tendendo a portale il CPI ad un valore unitario, senza mai raggiungerlo.
Il pipelining ha messo in evidenza i potenziali conflitti (hazard) che si vengono a creare, quando si intende
trasformare il ciclo sequenziale della Rete di Governo in un processo parallelo: tali potenziali conflitti
devono essere risolti ed occorre introdurre nuove tecniche per poterli risolverli al meglio, riducendo al minimo
l’uso di nop.
La strada successiva è quella di superare la barriera unitaria del CPI, introducendo più moduli di
elaborazione identici (ad esempio più di una ALU) in modo da poter trattare più di una istruzione per ciclo di
clok.
87
In questo modo si verrebbero a realizzare situazioni effettive di parallelismo che introdurrebbero ulteriori
potenziali conflitti, ad esempio nel caso di sequenza di istruzioni dipendenti, da trattare opportunamente.
Le architetture che prevedono di superare la barriera di 1 CPI vengono dette superscalari identificando in
questo termine la possibilità (teorica) di raddoppiare il numero di istruzioni eseguibili per ciclo di clock
semplicemente raddoppiando i necessari moduli di elaborazione. Per far questo, dovrà essere introdotta una
fase ulteriore nel ciclo della RG che smisti le istruzioni decodificate alle unità di elaborazione attualmente
libere evitando potenziali conflitti.
88
6.2 Memoria e prestazioni dei Computer
La memoria, così come presentata finora può essere realizzata secondo diversi principi e tecnologie e,
soprattutto, a costi estremamente differenti.
Per poter classificare la memoria dobbiamo evidenziarne una specifica caratteristica utile all’osservatore e
quindi determinarne degli attributi che ne evidenzi l’appartenenza ad una determinata classe.
La seguente tabella illustra le caratteristiche più importanti:
Caratteristica Classificazioni
Distanza dal processore Processore/Interna/Esterna
Capacità Dimensione parola x numero parole
Unità di trasferimento Parola/Blocco
Metodo di accesso Sequenziale/Diretto/Casuale/Associativo
Prestazioni Tempo di accesso/ciclo, velocità di trasferimento
Tecnologia fisica Semiconduttore/magnetica/ottica
Comportamento fisico Volatile/non volatile, Riscrivibile/Sola lettura
Organizzazione Come i bit formano una word
Una classificazione pragmatica della memoria si basa su tre caratteristiche “pratiche”: Capacità, Prestazioni,
Costo.
La figura seguente fornisce una tassonomia “triangolare” basata sulla capacità:
16 5 nS
Registri Capacità
Tempo di accesso
Costo 1 MB 20 nS
Cache
Principale 1 GB 80 nS
Memory
Magnetica 1 TB 10 mS
Dal diagramma si può vedere come i registri ovvero quelle particolari memorie interne alla CPU siano i più
costosi e i più veloci:
- Veloci, perché essendo nello stesso chip della CPU, non si deve accedere ad un bus esterno per
leggerli o scriverli.
- Costosi perché contribuiscono alla dimensione fisica totale del chip e, come abbiamo visto al par. 2.2.1,
la dimensione è legata al costo in secondo una dipendenza quadratica.
Il numero di registri, peraltro, è strettamente legato al set di istruzioni utilizzato e quindi non può essere
variato a piacere.
Scendendo verso il basso, abbiamo la memoria “Principale”, tipicamente in tecnologia “DRAM” in quando
consente di avere una grossa quantità di memoria in poco spazio (1/5 della SRAM) e quindi ad un costo/bit
più basso ma prestazioni molto più basse.
Per poter garantire una buona velocità di elaborazione della CPU, nelle istruzioni di accesso alla memoria
principale, la soluzione è abbinare a tale memoria una piccola memoria veloce “SRAM” chiamata "cache".
89
6.2.1 Memoria Cache
Questo principio è basato sulla considerazione che il codice di un programma ha le seguenti caratteristiche:
Ad eccezione dei salti, la maggior parte delle istruzioni sono contigue e quindi vengono eseguite in
successione (località spaziale).
Spesso i programmi sono costituiti da dei cicli da eseguire più volte (località temporale)
Mentre, per l’accesso ai dati, si hanno questi comportamenti predominanti:
Un blocco di codice tende a riferirsi alla stessa locazione per molte volte (località temporale)
Gli elementi di un vettore vengono spesso trattati in sequenza (località spaziale)
in tale formula si può vedere che, se h tende ad 1, allora il tempo di accesso tende a c, mentre, se h tende a
0, allora il tempo di accesso tende a m, aumentato del tempo c (che normalmente è molto più basso di m);
questo in quanto, per accedere ad una locazione di memoria, prima la CPU accede alla cache, impiegando
un tempo c, quindi accede alla memoria principale impiegando un tempo m.
La cache è organizzata in blocchi di locazioni chiamati "cache line". Questo grazie al principio di località
consente di aumentare il "hit ratio" in quanto può accadere che, anche la prima volta, la locazione da
elaborare si trovi già in cache. Il funzionamento della cache è quindi il seguente: se la locazione cercata non
è in cache, vengono caricate dalla memoria principale un blocco di parole (una cache line); se la locazione
da accedere successivamente è contigua, la CPU la troverà già pronta in cache. Per questo, l'architettura
del sistema deve consentire un accesso rapido a blocchi di memoria: ad esempio, nel Pentium, la memoria è
accessibile in due parole consecutive (64 bit) per cui un'unica operazione di read è in grado di caricare una
cache line di 8 bytes.
90
Un'altra problematica intuitiva concernente la cache sta nella sua dimensione: aumentando la dimensione
aumenterebbe la probabilità di trovare le locazione necessarie nella cache ma aumenterebbe purtroppo il
tempo necessario per controllare se la locazione è o non in cache.
Per affrontare tale problema, si usano più livelli di cache gerarchici in modo da bilanciare opportunamente
le esigenze di dimensione, costo e velocità di accesso secondo lo schema generico seguente:
Dove, al livello L1 vi sono due cache separate (split cache) per il programma e per (split cache) in modo da
consentire l'esecuzione contemporanea delle fasi di fetch e get-operand, in una architettura pipelined.
La cache L1 è integrata nello stesso chip della CPU e quindi beneficia di tutti i vantaggi di una connessione
estremamente veloce ed è relativamente piccola (tipicamente 16 o 64 KB).
A livello L2, in una posizione prossima alla CPU (magari nello stesso package) troviamo una generica cache,
sempre a connessione veloce ma con dimensioni decisamente maggiori (dai 512KB a 1MB).
Al livello L3, infine, possiamo trovare diversi MB di SRAM, che permette comunque un accesso molto più
veloce di quello consentito dalla DRAM.
nell'esempio in figura, la cache contiene 2K cache line ed ognuna è costituita da 32 byte (cache da 64 KB).
Ogni cache line contiene i tre campi:
- Valid (1 bit) indica se la linea è valida oppure no
- Tag (16 bit) identificativo univoco della linea di memoria di origine
- Dati (32 byte) copia dei dati ella memoria
Per effettuare la ricerca della linea nella cache, si scompone l'indirizzo relativo nei seguenti campi:
- Tag (16 bit) deve corrispondere al valore del campo TAG nella cache-line
- Line (11 bit) individua direttamente una delle 2048 cache line
- Word (3 bit) indica quale parola da 32 bit delle 8 presenti nella linea viene referenziata
- Byte (2 bit) normalmente è 0, ma se si richiede un singolo byte, viene indicato quale tra i 4 di una parola
a 32 bit
Il funzionamento di tale schema è immediato è diretto: una volta ricavata la cache line dal campo "line", si
controlla se i tag coincidono; in caso positivo (cache hit) si accede alla cache, altrimenti (cache miss) si
sostituisce l'attuale linea con quella richiesta dalla memoria. Ovviamente, se i dati sono stati modificati,
occorre riscriverli in memoria principale, prima di sovrascrivere.
L'importanza di tale organizzazione è che, seguendo i paradigmi della località spaziale, consente di
mantenere in cache fino 64KB di memoria contigua e si ha un sovraccosto di ricerca (hit/miss) praticamente
nullo (accesso diretto).
Lo schema funziona male nei casi in cui si acceda alternativamente tra locazioni di memoria distanti 64 K (ad
esempio un confronto tra stringhe contenute in due zone di memoria distanti 64K).
Una soluzione a questo problema consiste nel permettere più cache-line per indirizzi con lo stesso valore del
campo "line" come nello schema seguente.
Lo schema di esempio mostra una cache 4-way set-associative ovvero una cache in cui sono previsti 4
cache-line per ogni indirizzo di linea.
Questo tipo di cache, introduce, oltre che una maggiore complessità, anche un sovraccosto per ricercare la
cache-line richiesta tra le n-possibili. Normalmente, si usano schemi 2-way o 4-way in maniera efficiente
potendo eseguire in parallelo il confronto sui 4 tag di ciascuna linea.
Un'ulteriore problematica viene introdotta nel caso la linea sia completa ed occorra scegliere quale "cache-
line" sovrascrivere. In questo caso si utilizzano politiche di rimpiazzamento del tipo LRU (Least Recently
Used - si scarta quella usato meno recentemente) sempre seguendo il principio della località dei riferimenti.
92
L'efficacia dell'algoritmo LRU rispetto ad altri algoritmi diversi é stata dimostrata sperimentalmente, ma può
anche essere facilmente compresa in termini intuitivi. Basta pensare che non tutte le celle di memoria
godono effettivamente della proprietà di località nel tempo. Quindi, se dopo aver inserito la copia di un dato
in Cache, riscontriamo che durante un intervallo di tempo ragionevolmente lungo la CPU non ha più richiesto
l'accesso a quel dato, potremmo ragionevolmente supporre di esserci sbagliati nell'ipotizzare che valesse la
proprietà di località per l'accesso a quella cella. D'altra parte l'esecuzione dei programmi avviene
normalmente "per fasi successive" con caratteristiche diverse, per cui anche le proprietà di località
nell'accesso ai dati possono cambiare tra una fase e l'altra; quindi il fatto che, anche dopo ripetuti accessi
allo stesso dato, il programma non vi acceda più durante un lungo intervallo di tempo successivo, potrebbe
significare che prima quel dato godeva della proprietà di località ma ora non più. Per contro, se dopo aver
inserito la copia in Cache osserviamo che questa é stata effettivamente riutilizzata altre volte, anche in un
passato molto recente, allora troviamo conferma alla nostra ipotesi che per quella cella valga effettivamente
(anche adesso) la proprietà di località e ci aspettiamo che, ancora una volta, la CPU stia per ripetere entro
breve tempo l'accesso a tale dato; pertanto é consigliabile evitare, per quanto possibile, di togliere
quell'elemento "buono" dalla Cache per inserirne un altro la cui "bontà" (in termini di effettiva sussistenza di
caratteristiche di località) non é ancora dimostrata.
Ogni volta che viene aggiornata una locazione nella cache, si viene a creare una situazione di inconsistenza
tra cache e memoria principale.
Per evitare questo, la soluzione più semplice da adottare è quella di scrivere sia sulla cache che sulla
memoria principale (write through). Come ogni soluzione semplice, ha come svantaggio quello di richiedere
un notevole traffico di dati verso la memoria principale (ad esempi nei casi di accumulazione su determinate
locazioni di memoria).
Per valutare meglio l'importanza del numero di istruzioni di scrittura rispetto a quelle di lettura nell'efficienza
di una soluzione write-through possiamo analizzare un caso limite. Consideriamo un sistema "ideale" nel
quale sia la CPU che la Cache sono in grado di svolgere i loro compiti in tempo nullo (ossia di contribuire
all'esecuzione dei programmi con una velocità potenzialmente infinita), mentre la memoria RAM ha un
tempo di accesso non nullo che, senza nulla perdere in generalità del ragionamento, possiamo ipotizzare di
una unità di tempo per ogni accesso. É evidente che questo esempio rappresenta un caso in cui l'effetto
dell'introduzione della Cache dovrebbe essere massimo. L'esecuzione di un programma che richieda il
completamento di R operazioni di lettura e di W operazioni di scrittura, comporterà un tempo di
completamento di Tram=R+W unità di tempo in un sistema che non faccia uso della Cache. In un analogo
sistema contenente una Cache di tipo write-through lo stesso programma richiederà un tempo di esecuzione
Tcache maggiore o uguale a W unità di tempo (il tempo sarà in generale maggiore perchè non abbiamo
tenuto conto del caricamento iniziale dei dati dalla RAM alla Cache).
Quindi anche in questa sistema ideale, e pur nell'ipotesi eccessivamente ottimistica che l'introduzione della
93
Cache possa accelerare tutte le operazione di lettura, non ci possiamo aspettare un aumento di velocità di
esecuzione dovuto all'introduzione della Cache superiore a (R+W)/W. In pratica studiando le proprietà dei
programmi con tecniche statistiche, si possono misurare numeri di operazioni di lettura da 2 a 4 volte
superiori al numero di istruzioni di scrittura. Di conseguenza, in media non ci possiamo aspettare aumenti
di velocità di esecuzione dei programmi superiori a valori compresi fra 3 e 5 se adottiamo una cache di
tipo write-through con elevato livello di associatività, elevato numero di linee e velocità molto superiore a
quella della RAM.
L'alternativa a questa scelta progettuale è quella di differire la scrittura fino a quando possibile (write
deferred o write back) ovvero fino a quando non occorra rimpiazzare una cache-line oppure non vi sia un
errore con corrispondente dump della memoria oppure ancora una richiesta di quella locazione da parte di
un altro processore o periferica.
Per i salti incondizionati, si può intervenire seguendo due strategie utilizzabili, sia per la gestione della
pipeline che per la memoria cache:
Per trattare i salti condizionati, prendiamo come modello il seguente segmento di codice java-like e la sua
traduzione in linguaggio assembly
La compilazione mette in evidenza la criticità del codice: su 5 istruzioni 2 sono di salto e la sequenza più
lunga è di sole due istruzioni: La pipeline non verrebbe mai alimentata.
Il salto incondizionato potrebbe essere trattato con la tecnica del delay SLOT, semplicemente invertendo
l’ordine delle due istruzioni LDI R17,1/RJMP next con RJMP next/ LDI R17,1
94
c) Prevedere (Branch Prediction) se il salto verrà preso oppure no.
Per migliorare la previsione “statica” contemperando i costi, sia delle risorse da utilizzare, sia del ripristino
(roll-back) da effettuare in caso di errata previsione, si possono usare le seguenti tecniche:
a) a livello di compilatore, fare un’analisi semantica sulle istruzioni di alto livello, (esempio capire se
deve essere eseguito un ciclo for) e comunicarlo in qualche modo alle MV inferiori, usando gli
strumenti messi a disposizione (se ce ne sono). Ad esempio, UltraSPARC II ammette nelle istruzioni
di salto condizionato di indicare opzionalmente se il branch sarà o no preso.
b) Effettuare il “profiling” del programma facendolo eseguire da un emulatore che osserva dove e
quando i salti sono presi e comunica tali informazioni statistiche al compilatore.
La History table dovrà contenere tutti i dati necessari per poter prendere la decisione ovvero saltare in
anticipo alla locazione di destinazione.
Nel caso il processore preveda dei salti ad indirizzi calcolati dinamicamente, si ha il doppio problema di
prevedere il Branch/NoBranch ed anche, nel caso di Branch, l’indirizzo di destinazione.
Per poter gestire anche queste casistiche è necessario aggiungere tale informazione in ogni riga della
tabella, come da schema seguente:
96
7. Architetture Superscalari
In queste architetture il paradigma principale si basa sulla "instruction issue" ovvero sulla distribuzione (invio)
delle istruzioni in un microprocessore alla fine della fase di decoding, dirette alle unità di esecuzione.
Per aumentare la velocità di elaborazione di un processore, si deve agire in armonia secondo due aspetti:
aumentare il parallelismo per la “instruction issue”
aumentare il parallelismo in esecuzione
Partendo dall’architettura della macchina di Von Neumann tradizionale, in cui ogni singola istruzione veniva
eseguita secondo fasi sequenziali, per aumentare le prestazioni dei processori ci si è orientati nel cercare di
parallelizzare l’esecuzione delle singole fasi del ciclo della rete di governo. Sono quindi emersi, in fase di
progettazione, due concetti ortogonali per il parallelismo: unità di esecuzione multiple (senza pipeline) o
utilizzo del pipeline. In ogni caso si fa strada il concetto di ILP (Instruction Level Parallelism).
Il parallelismo a livello di istruzioni, potrebbe essere ulteriormente estremizzato, intervenendo sul vincolo
progettuale che limita la frequenza di clock al tempo di esecuzione dell’UF più lenta. Se vi sono delle UF che
possono rispondere alla doppio ella frequenza, si può pesare di progettare delle “Super-pipeline” a
frequenza doppia, ovvero eseguire due stadi di pipeline ogni ciclo di clock, suddividendo le UF in due
sottounuità più semplici le cui funzioni possono essere svincolabili ovvero non sovrapponibili, come da
figura:
97
Il grado di parallelismo si può estendere anche introducendo delle unità di esecuzione multiple controllate da
una pipeline e quindi verso un primo approccio superscalare:
Aumentando il parallelismo nell’esecuzione, si arriva al punto limite in cui la distribuzione sequenziale delle
istruzioni non è più in grado di rifornire con sufficiente velocità le pipelines relative alle unità di esecuzione
multiple. La distribuzione delle istruzioni (Instruction issue – tradotto anche in “attacco delle istruzioni”)
diventa quindi il “collo di bottiglia” da superare. (Ma questo Flynn l’aveva già predetto!).
Si arriva così allo sviluppo di architetture che consentissero una distribuzione parallela delle istruzioni
(Superscalari ILP). La prima implementazione prevedeva semplicemente l’utilizzo di istruzioni molto lunghe
(VLIW), costruite staticamente, che permettevano di eseguire operazioni multiple. Successivamente, ci si è
orientati nella possibilità di combinare in run-time più istruzioni in modo da poterle eseguire parallelamente e
quindi poter distribuire più istruzioni per singolo ciclo di clock.
Benché l’idea di distribuzione superscalare era stata formulata fin dal 1970, questo concetto fu preso in
considerazione solo agli inizi degli anni ’80, con i progetti “Cheetah” (‘82-‘83) e “America” (’85) di IBM,
seguita da DEC con il progetto “Multititan (’85). Questi ed altri progetti di ricerca portarono allo sviluppo
commerciale indicato nella tabella di pagina seguente. Tra parentesi viene indicato, per ogni processore il
numero di istruzioni distribuite per ciclo macchina (issue rate).
98
Processori 1989 1990 1992 1993 1994 1995 1996
Intel 960 960CA(3 960MM(3) 960HA/D/T(3
Intel x86 ) Pentium(2) PentiumPro(2 )
)
IBM Power RS/6000(4 Power2(4)
IBM ES ) ES/9000(2)
PowerPC Power 601(3) PwrPC 604(4) PwrPC 620(4)
Power 603(3) PwrPC 602(2)
Motorola 8800 MC88110(2)
Motorola 68K MC68060(3)
DEC Alpha 21064(2) 21064A(2) 21164(4)
HP PA PA 7100(2) PA 7200(2) PA 8000(4)
Sun/Sparc SuperSparc(3 UltraSparc(4)
)
TRON Gmicro Gmicro/500(2 PM1(Sp64)(4
) )
Mips R R8000(4) R10000(4)
AMD 29K AM29000(4)
AMD K5 K5 (~2)
Cyrix M1 M1(2)
NexGen Nx Nx586(1/3)
Dei prodotti elencati, solo due (Intel 960 e Am 29000) sono per uso “Embedded”, mentre gli altri sono di uso
generico. In genere sono tutti di tipo RISC ad eccezione di Intel x86, ES/9000, MC68000, Gmicro, K5, M1 e
Nx. In particolare K5, M1 e Nx586 sono compatibili con Intel x86 e sono stati sviluppati per competere con il
mercato Intel.
In sostanza, le due istruzioni possono essere “lavorate” in parallelo fino all’execute in quanto la i1 deve
(almeno) attendere il termine dell’istruzione i0 (In una pipeline scalare il problema può essere risolto
bypassando la fase di WO).
Tale dipendenza ha effetti più gravi per i processori superscalari in quanto il ritardo stesso viene amplificato
dalla mancata esecuzione di due istruzioni (invece della singola per il caso scalare)
100
7.3.4 Politica di distribuzione (attacco) e ritiro (completamento) delle istruzioni
La principale politica da adottare per aumentare lo “issue rate” è quella di scegliere se eseguire le istruzioni
“non bloccate”, alterando quindi l’ordine stabilito originariamente dal programma.
Più in generale, le scelte da fare sono le tra seguenti combinazioni:
a) distribuzione (issue) ordinata e ritiro (completion) ordinato
E’ una politica elementare a basso issue-rate e si usa come riferimento. Nell’esempio viene
schematizzata un’architettura superscalare con una doppia UF di decode una tripla UF di execute ed
una doppia UF di WO. si assuma che:
I1 richiede due cicli; I3 e I4 necessitano della stessa UF; I5 ha una dipendenza su dati da I4 I5 e I6
necessitano della stessa UF.
Se si vuole mantenere l’ordine, sia in distribuzione che in ritiro (WO), abbiamo i seguenti “blocchi”:
a) Finché I1 non è completata (2 cicli) non si può portare a ritiro nemmeno la I2
b) Le I3 e le I4 non possono essere eseguite in parallelo e quindi bloccano la distribuzione delle I5 e I6
fini al termine della I4,
c) Allo stesso modo, la I5 e la I6, usando la stessa UF, devono essere eseguite in sequenza e quindi
portate a ritiro ordinato
Per eseguire quindi 6 istruzioni occorrono ben 8 cicli di clock.
101
Per attivare tale strategia occorre stabilire un limite al numero di istruzioni che possono essere anticipate alle
UF di execute che quindi vengono poste in una “finestra di alimentazione” indipendente dallo stadio di
decode che quindi può continuare a ritmo “pieno”.
Questa separazione consente di anticipar l’esecuzione della I6, prima della I5 ed insieme alla I4.
Questo consente di ottener un issue-rate unitario, anche partendo da pipeline vuota ovvero la produzione
netta a 6 istruzioni in 4 cicli di clock.
Negli esempi precedenti non sono state prese in considerazioni gli ulteriori controlli da effettuare prima di
poter decidere se procedere alla distribuzione/ritiro disordinato.
Per fissare meglio le idee, usiamo ora un esempio più concreto, usando una scoreboard per evidenziare le
dipendenze sui dati
Nella scoreboard possiamo vedere se un registro è stato preso in esame da un’istruzione e per quanto ne ha
bisogno.
L’istruzone n.4 provoca un blocco nello stadio di decode in quanto si ha una dipendenza vera su R4:
102
Lo stadio rimane quindi bloccato finché R4 non viene scritto
103
Il blocco dell’I4 non blocca lo stadio di decode che prosegue quindi con l’I5.
I6 può proseguire se, invece di scrivere su R1 scrive su un registro “copia” (S1) ottenuto come “renaming” (e
quindi facilmente ripristinabile)
Sempre al Cy3, può essere ritirata la I2, senza attender il completamento della I1 (che era una
moltiplicazione)
L’istruzione 7 non può essere eseguita perché ha una dipendenza RAW sul registro “rinominato” S1 ma non
blocca la distribuzione della I8 purché si rinomini un’altra copia di R1.
In definitiva , le 8 istruzioni vengono eseguite in 9 cicli, 4 netti
104
7.3.5 Altre politiche di distribuzione delle istruzioni
Analizziamo i due maggiori aspetti della distribuzione: Le politiche di distribuzione (issue policy) ovvero come
gestire le dipendenze durante il processo di distribuzione ed il tasso di consegna (issue rate) ovvero in
numero di istruzioni che un processore può consegnare per ciclo di clock. Le politiche di distribuzione sono
abbastanza complesse e conviene rappresentarle usando i diagrammi “DS” (Design Space) in cui si
evidenziano le scelte ortogonali (ramificazioni ad angolo retto) e quelle alternative (rami obliqui):
Dalla figura si può notare che si hanno 4 aspetti principali da considerare come “ortogonali” e per questi
aspetti si hanno due possibili scelte alternative di progetto.
I primi due aspetti si riferiscono a come risolvere le false dipendenze sui dati e le dipendenze di controllo. Le
possibili alternative sono, rispettivamente:
- Usare o non usare il “register renaming” ovvero cambiare di nome ai registri per superare la dipendenza sui
dati.
- Usare o non usare la possibilità di processare i salti in maniera “presumibile” (speculative).
Il terzo aspetto determina come usare la tecnica avanzata della “scaffalatura” (shelving) per ridurre
drasticamente i bloccaggi di distribuzione dovuti alle dipendenze.
L’ultimo aspetto specifica come trattare i bloccaggi di distribuzione.
Vediamo ora in dettaglio i quattro aspetti:
Questa tecnica fu inventata negli anni ‘70, dimenticata e quindi reinventata nel 1990. Senza uso di shelving,
la distribuzione delle istruzioni rimane bloccata in caso di dipendenza finché questa non viene risolta. Il test
di dipendenza avviene usando una finestra che contiene n istruzioni da distribuire, con n = issue-rate.
In assenza di dipendenza, tutte le istruzioni della finestra sono inviate alle unità di esecuzione. In caso
contrario si ha il blocco della distribuzione che causa un notevole degrado di prestazioni.
La tecnica dello “shelving” disaccoppia la fasi di test di dipendenza da quella della distribuzione. Per questo
vengono impiegati dei buffer speciali, chiamati spesso “Stazioni di prenotazione” prima delle unità di
esecuzione. Le istruzioni vengono inviate a queste stazioni senza alcun controllo di dipendenza.
Successivamente vengono prelevate ed inviate alle unità di esecuzione quelle istruzioni che non presentano
dipendenza. Questa ritardo nell’invio delle istruzioni alle unità di esecuzioni viene chiamato “dispatching”
anche se il termine viene spesso usato genericamente come sinonimo di “issuing”.
I due scenari, senza shelving e con shelving sono illustrati nelle figure seguenti:
106
7.3.9 Trattamento del blocco della distribuzione
Il blocco può essere gestito in due modi: agendo sull'ordine o sull'allineamento delle istruzioni da
distribuire.
Agendo sull'ordine delle istruzioni abbiamo le due possibilità indicate nella figura seguente :
Nel caso a), le istruzioni vengono distribuite mantenendo l'ordine stabilito dal programma. Nel caso b)
invece, viene distrutto l'ordine prestabilito dal programma e vengono distribuite le istruzioni indipendenti.
E' agevole intuire come la scelta strategica "in-order" possa penalizzare pesantemente la performance
complessiva in quanto una singola istruzione dipendente può bloccare tutta la "issue window".
Infatti alcuni processori che utilizzano la distribuzione diretta (MC88110 e PowerPC601) hanno introdotto la
distribuzione "out of order". In questo schema un'istruzione viene distribuita anche se la precedente
istruzione dipendente ancora non è stata distribuita ma tale schema è previsto solo per poche instruzioni in
entrambi i processori.
Ad esempio, il PowerPC 601 distribuisce "out of order" le istruzioni di salto ed in virgola mobile mentre il
MC88110 solo le istruzioni in virgola mobile.
D'altra parte, perché solo pochi processori hanno la possibilità di distribuire le istruzioni "out of order"?
Vi sono essenzialmente due ragioni:
- Il distribuire "out of order" costringerà un maggior impiego di risorse per ricondurre poi l'esecuzione
secondo l'ordine prestabilito dal programma.
- I processori che utilizzano lo shelving non traggono molto vantaggio dalla distribuzione "out of order" in
quanto raramente ci si troverà in una situazione di blocco. L'utilizzo dell'"out of order" quindi, così come
previsto dalla legge di Amdahl aggiunge un beneficio marginale.
Da queste considerazioni ci aspettiamo quindi che i processori superscalari continueranno ad usare in futuro
una distribuzione "in-order".
Agendo sull'allineamento abbiamo le due possibilità indicate nella figura seguente:
107
Nel caso a) viene usata una distribuzione allineata tramite l'impiego di una window fissa: finché non vengono
distribuite tutte le istruzioni della window non è possibile distribuire altre istruzioni.
L'utilizzo della distribuzione allineata è tipica della prima generazione di processori superscalari (i960 e
Power1). Se viene usata la distribuzione diretta, l'allineamento riduce considerevolmente la velocità di
distribuzione.
Per questo, la maggior parte dei processori superscalari successivi che utilizzano ancora la distribuzione
diretta, hanno introdotto la distribuzione disallineata delle istruzioni. Per ottenere questo, si usa una window
"scorrevole" (vedi figura b) di ampiezza pari alla velocità di distribuzione. In ciascun ciclo tutte le istruzioni
della wndows sono controllate per la dipendenza: quelle indipendenti vengono distribuite (in order o out-of-
order) e quindi la window viene riempita di nuovo facendola scorrere di tante posizioni quante sono le
istruzioni che sono state distribuite.
Come detto precedentemente, la distribuzione disallineata è tipica di quei processori superscalari di seconda
generazione che utilizzano la distribuzione diretta (MC88110, MC68060, PA 7200, R8000, UltraSparc).
Con l'introduzione dello shelving anche qui l'importanza della window scorrevole è diminuita di molto. Di
conseguenza, gli ultimi processori che utilizzano lo shelving sono ritornati ad usare la distribuzione
"allineata" (PowerPC 603, PowerPC 604, PowerPC 620, and R10000).
La tabella seguente riassume l'orientamento dei costruttori rispetto alla distribuzione "allineata":
108
7.3.10 Le politiche di distribuzione più usate e rispettivo trend
La figura seguente riassume le politiche di distribuzione più usate nei processori scalari e superscalari.
Ovviamente tutte le maggiori famiglie di processori evidenziano lo stesso cammino di evoluzione consistente
in: Tradizionale scalare, Tradizionale scalare con esecuzione speculativa, superscalare diretta con o senza
allineamento, Superscalare avanzata. Le prestazioni aumentano all'incirca nella stessa direzione.
109
7.3.11 Il tasso di distribuzione
Quante istruzioni vengono distribuite per ciclo di istruzione (issue rate) ci dà la misura del grado di
superscalarità di un processore. Intuitivamente un più alto tasso di distribuzione indica, potenzialmente, delle
prestazioni migliori ma l'0implementazione ne risulta notevolmente più complessa.
Questo fornisce un limite per i processori CISC che limitano il tasso a 2 o 3 istruzioni per ciclo. I processori
RISC recenti, tipicamente distribuiscono 4 istruzioni per ciclo.
Nella figura seguente, viene evidenziata l'evoluzione del tasso di distribuzione in 5 famiglie di processori.
Come si può vedere, i primi processori ALPHA (21064, 21064A) ed i primi HP "Precision Architecture" (PA
7100, PA 7200) erano comunque limitati ad avere 2 istruzioni per ciclo. 3 istruzioni per ciclo si cominciano ad
avere con i primi PowerPC (601, 603) ed il SuperSparc. I modelli più recenti partono da 4 istruzioni per ciclo
tranne il Power 2 che distribuisce 6 istruzioni per ciclo
110
8. Architetture dei Computer Paralleli
I limiti di performance del parallelismo a livello di istruzione, possono essere superati moltiplicando intere
CPU facendole lavorare in una sincronia il più possibile efficiente.
Per questo è indispensabile che il software sia progettato in modo tale da limitare al massimo le dipendenze
di tipo sequenziale, dipendenze che ormai conosciamo bene come essere il principale ostacolo al
parallelismo ILP.
Per progettare un’architettura di Computer Paralleli, le principali problematiche da risolvere sono:
qual è la natura e la dimensione degli elementi computazionali (P);
qual è la natura, la dimensione e la numerosità degli elementi di memoria (M);
come interconnettere tra loro gli elementi di memoria e calcolo (P-M) definiti sopra
Le tre problematiche non possono essere affrontate distintamente perché non sono indipendenti: più a
struttura di interconnessione è “lasca” (accoppiamento “debole”) tra gli elementi P e M, e più deve essere
complesso (a “grana grossa”) ciascun elemento P-M e quindi più complesso dovrà essere il software che
potrà sfruttare al meglio tale architettura.
La relazione di dipendenza tra granularità ed accoppiamento può essere illustrata dalla seguente figura:
Dove, alla base, abbiamo un accoppiamento stretto e delle unità di calcolo elementari (ALU) ovvero
ritroviamo proprio l’ILP visto nei paragrafi precedenti.
Per affrontare al meglio la correlazione tra software e hardware e quindi progettare l’architettura di Computer
Parallelo ideale per una determinata tipologia di software, occorre prima definire i possibili modelli
architetturali e le strutture di interconnessione.
I modelli architetturali di riferimento possono essere classificati in due tipologie distinte: Multiprocessori o
Multicomputer.
Tutte le CPU, intese come unità di calcolo complete di rete di governo ed unità funzionali, condividono la
stessa memoria, secondo il seguente schema e relativa esemplificazione software:
Nella esemplificazione software, un’immagine grafica viene suddivisa in 16 parti ed ogni parte viene poi
111
assegnata a ciascuno dei 16 processori (P) interconnessi. Attraverso meccanismo di “mappatura” della
memoria condivisa ciascun processore può, sia elaborare la propria immagine considerandola come “locale”
ovvero “privata”, sia accedere all’intera memoria, utile nel caso di un’operazione di “rotazione” dell’immagine
dove dei pixel possono essere traslati nella casella adiacente. I meccanismi di coordinamento sono minimi
ed avvengono comunque utilizzando semplici strutture in memoria. Ad esempio se si volesse determinare la
dimensione della casa, è necessario solo decidere quale processore debba elaborare l’immagine e quindi
acceda alla memoria circostante.
La semplicità della programmazione e del coordinamento viene pagata da una complessità realizzativa che
richiede un forte accoppiamento tra M (che deve disporre, in questo caso, di 16 porte di accesso) e P.
Io forte accoppiamento condiziona quindi anche la scalabilità del sistema: aggiungere o togliere un
processore ha un grosso impatto, sia sul software che sull’infrastruttura hardware.
Anche qui il software suddivide un’immagine in 16 parti ed ogni parte viene poi assegnata a ciascuno dei 16
computer (P-M) interconnessi. Ciascun processore elaborare la propria immagine considerandola nella
propria memoria locale ed utilizza l’infrastruttura di comunicazione per inviare messaggi (send) ad altri
computer e rivedere (receive) i messaggi provenienti dagli altri. Nel caso di un’operazione di “rotazione”
dell’immagine i pixel eccedenti devono essere trasmessi al processore che ha la gestione della relativa
casella. I meccanismi di coordinamento sono quindi più complessi, così come il relativo software. Ad
esempio se si volesse determinare la dimensione della casa, è necessario che uno dei quattro computer
coordini e che gli altri tre forniscano al coordinatore, le informazioni di propria competenza.
Tale complessità viene compensata da una maggiore semplicità realizzativa e da una più alta scalabilità: ad
esempio, poco cambia, sia nella realizzazione che nel software, se invece di 16 computer ne impiegassimo
15 o 17 o anche 1000.
112
Nella soluzione illustrata a sinistra, abbiamo realizzato un sistema “multiprocessore” che maschera la
complessità della gestione della memoria condivisa ai livelli inferiori al sistema operativo: se la memoria
richiesta dal processore non è ancora mappata nel sistema richiedente, si genera un’eccezione che
consente di caricare, con un meccanismo analogo a quello visto per la memoria cache, il blocco di memoria
richiesto.
Nella soluzione al centro, invece, è il sistema operativo che si occupa di gestire la memoria condivisa, con
un meccanismo analogo alla paginazione.
Nella soluzione più a destra, è il linguaggio stesso che gestisce la memoria condivisa, a livello di runtime .
La struttura di interconnessione viene chiamata più propriamente “rete” ed ha caratteristiche simili, sia nelle
architetture multicomputer che nei multiprocessori in quanto si basano sullo scambio di messaggi, che nel
caso di multiprocessori corrispondono a segnali trasmessi sul “control bus“. Per scambiare messaggi in rete,
questi sono gli elementi costitutivi essenziali:
Interfacce: dispositivi (chip o schede connesse al bus locale) che estraggono o inseriscono i
messaggi. Le interfacce potrebbero contenere anche delle CPU e memoria locale.
Link: canali fisici su cui vengono trasmessi i messaggi (rame, fibra ottica, onde radio, ..). I mesaggi
possono essere trasmessi in seriale (1 bit alla volta) o in parallelo (più bit). I link sono caratterizzati
dall’avere:
o una BANDA massima prefissata in bit/secondo o parole/secondo;
o un senso di percorrenza: semplice, alternato (half-duplex) o bidirezionale (full duplex), in
analogia con le percorrenze delle strade;
Switch: dispositivi con diverse porte di input e output (interfacce) alle quali sono collegati i link e che
sono in grado di instradare i pacchetti da un link ad un altro, in base al contenuto della “busta” che
contiene il pacchetto. Sono quindi analoghi agli incroci di una rete stradale, dove la direzione viene
(dovrebbe essere) stabilita dagli indicatori di direzione del veicolo.
113
Senza pretesa di essere esaustivi sul tema delle reti, richiamiamo i concetti base di questi aspetti, necessari
per comprendere l’importanza delle scelte progettuali nelle diverse soluzioni di architetture parallele e
valutare quindi le relative prestazioni sia da un punto di vista hardware che da un punto di vista software.
Pere semplicità ci limiteremo ad analizzare il caso di trasmissioni “seriali” dove i link trasmettono un bit alla
volta e dove quindi è definibile l’ampiezza di banda come la velocità massima di trasmissione in bit/secondo.
114
e) Griglia o Maglia: Ha dimensionalità due ed è facilmente scalabile in quanto il diametro,
all’aumentare dei nodi, cresce con la lunghezza dei lati della griglia ovvero con la radice quadrata
del numero di nodi.
f) Doppio Toroide: variante della griglia che rafforza la resilienza dei nodi periferici e diminuisce l
diametro.
g) Cubo: ha dimensionalità tre ed è molto regolare. Nell’esempio si è limitato il numero a 2*2*2 (= 8
3
nodi) ma si consiglia di vederlo generalizzato alla configurazione k * k * k (k nodi).
h) Ipercubo: ha dimensionalità N, ovvero è una generalizzazione del cubo che mantiene il diametro
N
pari alla dimensione, limitando il numero di nodi a 2 . Il diametro cresce quindi lentamente
all’aumentare dei nodi, come il log2 N. Per costruire tale topologia, si segue il procedimento illustrato
in figura: partendo dal cubo tri-dimensionale, lo si raddoppia e si connettono tutti i nodi
corrispondenti con un ulteriori link, aumentando di una unità la relativa dimensione ed ottenendo una
rete da 16 nodi; per costruire un ipercubo 5-d, si raddoppia la struttura 4-d e connettono i 16 nodi
aggiunti tra di loro e così di seguito. Ad esempio, un ipercubo di dimensione 10 avrà 1024 nodi ed un
diametro pari a 10, dando risposte eccellenti nella trasmissione dei messaggi, rispetto ad una
topologia a griglia 32 * 32 che avrebbe avuto un diametro pari a 31 + 31 = 62. Tale aumento di
prestazioni viene pagato da un incremento di costi, legati all’elevato fan out (10).
115
Per attuare tale strategia gli switch devono avere una memoria in grado di “bufferizzare” tutti i pacchetti in
ingresso per il tempo necessario per individuare un link utile per la trasmisisone e quindi trasmettere il
pacchetto. Poiché la memoria non può essere “infinita” occorre scegliere un’adeguata strategia di “buffering”
che massimizzi l’utilizzo della memoria, limitando al minimo il rischi di “saturazione”. LE strategia di base
sono:
a) Input buffering: i pacchetti in ingresso da una determinata posta vengono collocati in una struttura
a “coda“ di tipo FIFO (Firs In First Out): se la porta di uscita è occupata, il pacchetto rimane in
attesa coda bloccando quindi anche i pacchetti seguenti, con il rischio di bloccare pacchetti che
avevano altre destinazioni (problema di “head of line”).
b) Output buffering: per superare l’head-of-line, una possibile soluzione sta nell’associare la coda
FIFO alla porta di uscita. Rimane qui il problema di dover dimensionare la coda per evitare rischi di
“overflow” e quindi della conseguente perdita di pacchetti.
c) Common bufering: consente di mitigare i rischi di dell’overflow prevedendo un’allocazione
dinamica delle code. Il rischio viene mitigato ma rimane comunque e per questo ò opportuno
configurare gli switch in modo adeguato prevedendo, ad esempio, dei limiti sulla dimensione
massima dei pacchetti.
116
8.3 Misura delle prestazioni (hardware)
Usando i concetti introdotti nei paragrafi precedenti, siamo ora in grado di poter valutare, almeno
indicativamente, le prestazioni dell’infrastruttura di comunicazione, misurando la latenza e l’ampiezza di
banda in funzione delle possibili scelte progettuali.
La latenza è definita come il tempo necessario per inviare un pacchetto ed ottenere la risposta ovvero la
porzione di tempo “aggiunta” e che non è funzionale all’elaborazione stessa.
1) LATENZA
Tale parametro dipende dall’ampiezza di banda e dalla strategia di switching adottata.
a) Circuit switching: Tp = Ts + 2 * (p/b)
Dove:
Tp è il tempo di latenza totale per trasmettere un pacchetto composto da p bit ed ottenere la risposta
(full duplex) di p bit;
Ts è il tempo di “setup” necessario predisporre il circuito (esempio invio pacchetto di analisi e
predosposizione del circuito e quindi costruzione pacchetto dati);
p è la dimensione in bit del pacchetto complessivo da trasmettere, busta compresa
b è l’ampiezza di banda del circuito;
2) AMPIEZZA DI BANDA
Per misurare tale parametro in maniera consistente/effettiva, occorre rapportare la banda propria di ciascun
link all’intera struttura topologica della rete. Per questo abbiamo diverse metriche di riferimento da adottare,
tutte comunque imprecise ma che consentono comunque di avere una stima teorica, comunque sufficiente
per poter valutare le possibili alternative progettuali. Le principali metriche sono:
a) Bisezione dell’ampiezza di banda (già analizzata in precedenza)
b) Aggregate bandwidth: somma delle larghezze di banda proprie di tutti i link – indica il numero
massimo di bit che possono transitare nella rete;
c) Ampiezza Media Banda in uscita dalle CPU – è un parametro progettuale da considerare,
soprattuto per evitare clamorosi errori progettuali. Ad esempio, se la CPU ha una velocità massima
di 1 Mb/s, inutile collegarla ad una rete che garantisce una bisezione dell’ampiezza di banda pari a
100 Gb/s
117
8.4 Misura delle prestazioni complessive
Uno dei principali fattori che impediscono di ottenere uno speedup lineare deriva dalla oggettiva difficoltà nel
rendere “parallelizzabile” un software che, per sua natura ed origine è di tipo “sequenziale”. Tale difficoltà
può descritto dalla seguente formula, chiamata Legge di Amdahl:
T n
Speedup
Tp 1 (n 1) f
Dove:
n = Numero CPU
T = tempo necessario per eseguire il programma su un sistema monoprocessore
Tp = tempo effettivamente necessario per eseguire lo stesso programma su un sistema a n CPU
f = percentuale del tempo di elaborazione necessariamente sequenziale.
118
Per dimostrare la legge di Amdahl si scompone il tempo di esecuzione monoprocessore T del software in
due parti, come indicato nella figura (a) precedente: una frazione che deve rimanere intrinsecamente
sequenziale, indicata con f*T , ed il restante (1-f)*T che, potenzialmente, potrebbe essere parallelizzata.
Il tempo Tp, trascurando le eventuali latenze del sistema complessivo, necessario per eseguire il
programma, disponendo di n CPU, sarà quindi pari a:
Tp = f*T + (1-n)*T/n
Sostituendo tale espressione nella frazione T/Tp si ottiene la Legge di Amdahl che ci conferma
matematicamente che lo speedup lineare si potrà raggiungere solo per f 0 mentre la funzione si scosterà
fortemente al crescere di f, come evidente dalla grafico:
35
30 f=0
f=1%
25
f=5%
20 f=10
%
f=50
15 %
f=80
%
10
0
1 2 3 4 5 6 7 8 9 10111213141516171819202122232425262728293031
119
8.5 Classificazione delle Architetture Parallele
Per catalogare le possibili scelte progettuali, si utilziza ancor oggi la classificazione tassonomica introdotta
da Mike Flynn nel 1972:
Il successo di tale classificazione deriva dall’alto livello di astrazione utilizzato, ovvero dalla lasca dipendenza
dai possibili dettagli progettuali.
Tale astrazione riconduce la complessità allo schema fondamentale della Macchina di Von Neumann dove la
computazione consiste in due flussi (stream) principali: il flusso di istruzioni (I) ed il flusso dati (D). da questo
assunto discende quindi la prima grande classificazione:
SISD: Single Instruction, Single Data (stream): un singolo flusso di istruzioni agisce su un singolo insieme di
dati – è il funzionamento classico ela Macchina di Von Neumann monoprocessore;
SIMD: Single Instruction, Multiple Data (stream): un singolo flusso di istruzioni opera su più dati
simultaneamente (e quindi produce un flusso multiplo di dati) – è il funzionamento dei processori
“vettoriali”;
MISD: Multiple Instruction, Single Data (stream): Più processi operano sullo stesso insieme di dati – non è
una soluzione che trova implementazioni utili e va considerata quindi come “teorica”;
MIMD: Multiple Instruction, Multiple Data (stream): più processi operano contemporaneamente su più set di
dati (e quindi produce un flusso multiplo di dati) – è il funzionamento previsto per i multprocessori ed i
multicomputer, ramificazioni dalle quali discendono ulteriori classificazioni tassonomiche.
120
8.5.1 SIMD – Array e Vector processor
Le architetture SIMD sono tipicamente utilizzate nei supercomputer in quanto specificatamente orientate a
svolgere calcoli vettoriali per risolvere problemi matematici legati a dei processi fisici-ingegneristici
(aerodinamica, sismologia, meteorologia, fisica atomica e nucleare, …).
Non hanno quindi un mercato ampio e quindi il loro costo resta molto alto, nell’ordine dei 10 milioni di Euro.
La caratteristica comune è quella di avere un’unica unità di controllo che esegue un’istruzione alla volta ma
ciascuna istruzione opera su più dati contemporaneamente.
Le possibili implementazioni si distinguono in Array processor e Vector Processor.
1) Array Processor
2) Vector Processor
121
8.5.2 MIMD – MultiProcessori a memoria condivisa
Le architetture MIMD a multiprocessore prevedono di far coesistere più processori (CPU) che condividono
un unico spazio di indirizzamento.
Il coordinamento tra le diverse CPU avviene al livello di Sistema Operativo e quindi non entreremo in tali
dettagli implementativi ma ci limiteremo ad analizzare le caratteristiche architetturali osservabili al livello
MV2.
La prima caratteristica progettale da considerare è quella di prevedere una gerarchia ovvero una
specializzazione tra le diverse CPU oppure considerare tutte le CPU equipollenti ovvero in grado di accedere
a qualsiasi indirizzo di memoria o di I/O. Nel seguito ci occuperemo principalmente di sistemi con CPU
paritetiche, chiamati “SMP” ovvero di “Symmetric MultiProcessor”.
In tali sistemi, la classificazione Tassonomica prevede una prima diversificazione in base alla modalità di
gestione ed accesso ai diversi moduli di memoria: Uniforme (UMA), Non Uniforme (NUMA) o delegato alla
memoria Cache (COMA).
Oltre alla modalità di gestione della memoria occorre stabilire il modello “semantico” ovvero l’insieme di
regole che governano l’accesso alla memoria, regole alle quali chi scrive il software ai livelli superiori si deve
attenere (e che rendono quindi più o meno complessa la multiprogrammazione) e che possono essere più o
meno “rigorose” secondo la seguente scala di “consistenza” dove vengono descritte le caratteristiche
esterne, senza approfondirne l’implementazione in quanto fortemente correlata alle esigenze dei livelli MV
superiori:
Modelli a consistenza stretta: ogni locazione restituisce esattamente il valore scritto più recentemente.
E’ un odello semplice da programmare ma non lascia molte scelte progettuali in quanto si può ottenere
utilizzando un unico modulo di memoria ce serve le richieste di accesso in ordine, come piace agli
inglesi e per questo useremo la definizione “Fisrt Came First Served”. Tale modello rappresenta un
“collo di bottiglia” che non garantisce grosse prestazioni, così come già osservato nelle politiche di
“instruction issue”.
Modelli a consistenza sequenziale: in caso di accesso r/w concorrente ad un'unica risorsa, l’hardware
mette in sequenza “non deterministica” tali richieste, rendendoli quindi visibili alle diverse CPU/software.
Ad esempio, si abbia la seguente “rapida” successione (ad esempio a distanza di un nS –
nanoSecondo) di accessi alla stessa locazione x:
o CPU 1 scrive 100 (W100) e CPU2 scrive 200 (W200) su x;
o CPU3 legge due volte (R3, R3) e CPU4 legge due volte (R4, R4) da x;
Questi sei eventi possono essere ordinati sequenzialmente in diversi possibili modi, ottenendo risultati
tutti perfettamente legittimi:
o W100 W200 R3(200) R3(200) R4(200) R4(200)
o W100 R3(100) W200 R3(200) R4(200) R4(200)
o W200 R4(200) W100 R3(100) R4(100) R3(100)
o ….
violerebbe la consistenza sequenziale un risultato sequenzialmente diverso per le sue CPU, del tipo:
o W100 R3(100) W200 R4(200) R4(100) R4(200)
ovvero, mentre sarebbe possibile leggere prima 200 e poi 100, per ritardi dovuti al completamento della
scrittura, tale sequenza di valori avrebbe dovuto essere identica per entrambe le CPU3 e CPU4
Modelli a consistenza lasca: vengono rimossi i vincoli e la sequenzialità che viene rispettata solo su
richiesta;
122
Una volta definita la modalità di accesso ed il modello di consistenza, possiamo analizzare quali sono le
diversa infrastrutture di interconnessione tra i moduli di memoria ed i processori, seguendo lo schema
tassonomico.
8.5.2.1 UMA a BUS
Le architetture SMP più semplici sono quelle che prevedono un unico BUS, come indicato nella figura
seguente, secondo tre livelli di miglioramento (a,b,c)
Lo schema base (a) è semplice ma poco scalabile in quanto l’unica risorsa “BUS”, a banda comunque
limitata, costringe ad inevitabili “code” per prenderne il controllo e quindi accedere alla memoria condivisa.
Nello schema (b), la situazione migliora, grazie alla cache inserita in ciascuna CPU che quindi limita
l’accesso al bus/Memoria condivisa solo in caso di cache-miss.
Lo schema (c) migliora ancora la situazione in quanto viene allocata una memoria “privata” su un bus
dedicato, ideale per gestire le costanti, il codice, e le variabili read-only.
In ogni, caso, per garantire la coerenza della cache, è necessario che i controller spiino il bus (snooping
cache) ed attuino il relativo protocollo tra quelli visti in precedenza.
Ad esempio, usando il “Write through” avremo il seguente comportamento da pare dei controlli locali e
remoti:
Più complesso ma più efficiente sarebbe implementare un protocollo di “Write-back” come il MESI, che
identifica le sigle dei 4 possibili stati della cache:
Modified: linea valida in cache e non in memoria ma non esiste nella cache delle altre CPU;
Esclusive: linea usata esclusivamente da una cache ed uguale alla memoria;
Shared: linea usata in più cache ed uguale alla memoria;
Invalid: linea contiene dati non validi
Con riferimento alla figura che mostra il diagramma degli stati previsti per il MESI, vediamo in dettaglio le
azioni eseguire in base ai diversi eventi, dove P è l’iniziatore e S sono le Snooping cache:
1. Read Miss: se nessun S ha tale line allora P carica la line e va allo stato Exclusive, altrimenti se S
risponde “Exclusive” allora P carica la line e S e P passano allo stato Shared altrimenti se S risponde
“Shared” allora solo P carica la line passa allo stato Shared altrimenti se S rispond “Modified” S passa
line a P e aggiorna la memoria e P e S passano allo stato Shared.
2. Write Miss: P invia il segnale “Read with Intent to Modify”, se nessun S risponde allora P carica line e
passa allo stato Modified altrimenti se S e “Modified” allora S scarica la line e passa allo stato Invalid e P
riparte da capo. Parallelamente, se altri S sono in stato Exclusive o Shared allora S passa allo stato
Invalid
3. Write Hit: se P in stato Exclusive allora P passa allo stato Modified altrimenti se P in stato Shared allora
S passano a Invalid e P passa allo stato Modified.
123
8.5.2.2 UMA a crossbar switch
L’architettura prende spunto dalle centrali di commutazione telefoniche. E’ un0infrastruutra non bloccante
che richiede un numero di crosspoint (interruttori) proporzionale al quadrato del numero di CPU. Nello
schema vediamo come questa infrastruttura consente a tre CPU di lavorare contemporaneamente grazie ai
3 diversi percorsi tra memoria e CPU.
124
8.5.2.3 UMA a switch multistrato
Per ridurre il numero di switch è possibile adottare la
soluzione derivata dalle infrastrutture di rete dove le
operazioni di lettura/scrittura sono inserire in pacchetti
“strutturati” come in figura, dove:
Module: indica il modulo di memoria da utilizzare;
Address: indica l’indirizzo all’interno del modulo;
Opcode: indica l’operazione da effettuare (Read o Write)
Value: Valore opzionale da scrivere ovvero valore di ritorno per letture.
Per avere buone prestazioni anche con centinaia di CPU, è necessario un approccio che consenta di
distribuire i moduli di memoria, come da schema seguente, adottato dal sistema CM* e che prevede due
livelli di bus:
Le architetture CC-NUMA riescono ad esse scalabili ma comunque hanno dei limiti nella gestione delle
pagine e le prestazioni comunque scendo nel caso di accesso RW frequente a locazioni remote.
Da tali limitazioni discende l’idea di eliminare la memoria locale ed usare solo la cache, senza avere una
memoria da cui attingere. Le linee di memoria si spostano verso le CPU che le utilizzano maggiormente
costituendo la cd “attraction memory”.
Il vantaggio evidente è quello di avere tutta la memoria complessiva ad alte prestazioni. Di contro, abbiamo
dei problemi nuovi da affrontare come:
- Dove trovare le line in caso di read miss?
- si aggiungono controller dei TAG di ogni cache line
- mappare le pagine intere anche se in cache vi e’ solo una parte
- Come evitare di eliminare una line non più utilizzata quando questa e’ l’ultima
- usare un direttorio delle cache line
- marcare una copia come copia “master”
126
8.5.3 MIMD – Multicomputer a scambio di messaggi
Dove:
- Ogni nodo consiste in una o più CPU, memoria locale, dischi ed altre unità di I/O;
- In ogni nodo vi è un processore dedicato a gestire i messaggi di comunicazione con gli altri nodi
- Si dispone di un’infrastruttura di comunicazione ad alta velocità
- Le interazioni sono effettuate, come prevedibile, con istruzioni LOPAD/STORE asociata a messaggi di
send/receive
I Multicomputer possono essere classificati nelle seguenti tipologie
Sono supercalcolatori dal costo elevato, da molti Milioni du Euro, Usano CPU standard (Pentium,
UltraSparc, DEC Alpha, ..) e una rete di interconnessione proprietaria a bassa latenza e larga banda.
Sono dotati di I/O molto veloce, dell’ordine dei terabyte e, normalmente si utilizzano librerie e software
proprietari, tra cui algoritmi dedicati a rilevare eventuali computer difettosi
Nel seguente esempio viene illustrato lo schema usato dal CRAY T3E:
Tale sistema impiega fino a 2048
CPU DEC Alpha 21164 connessi
in due modalità:
a) Un Toroide principale
tridimensionale full-duplex (in
basso)con link da 480 MB/s
b) un GigaRing al quale sono
connessi anche i dispositivi I/O e
dove transitano pacchetti da 256
bytes max.
Ogni 128 nodi ce ne e’ uno di
scorta che rimpiazza a caldo un
eventuale guasto.
Il S.O. è una variante di UNIX
127
8.5.3.2 COW – Cluster of Workstation
Molto più abbordabili nel prezzo e quindi molto diffusi sono i COW – Cluster of Workstation o NOW -
Network of Workstation.
Sono costituiti da componenti facilmente reperibili sul mercato ovvero usano PC o WS standard connessi in
rete commerciale (Ethernet)
Esistono molte tipologie di COW, tra le principali classificazioni ciotiamo:
- COW Centralizzato: è un cluster di WS o PC montati in un rack e posti in un’unica stanza cercando di
ottimizzare lo spazio (headless Workstation)
- COW Decentralizzato: WS e PC sono distribuiti su più stanze tramite una LAN di alta prestazioni (o
anche Internet), alcuni dei quali possono essere accesi o anche spenti.
128