Sei sulla pagina 1di 128

Appunti delle lezioni Teoriche di

Architettura degli elaboratori


INDICE
1. Approccio strutturato alle architetture dei calcolatori 4
1.1 Introduzione 4
1.2 Strutturazione verticale a livelli di Macchine Virtuali 6
1.3 Richiami sulla COMPILAZIONE e INTERPRETAZIONE 7
1.4 Livelli tipici di un Sistema di Elaborazione 9
1.5 L’equivalenza Hardware-Software 11
1.6 Strutturazione Orizzontale a MODULI nei livelli - generalità 11

2. La rappresentazione dell’informazione (A) 14


2.1 Richiami sul sistema di numerazione binario e conversione di base 14
2.1.1 Strutture fisiche GERARCHICHE di INFORMAZIONE 16
2.1.2 Rappresentazione binaria dei numeri relativi 17
2.1.3 Rappresentazione binaria dei numeri reali 18
2.2 Organizzazione delle memorie 20
2.3 Composizione delle strutture fisiche di informazione 20
2.4 I codici di caratteri 21
2.4.1 Codice BCD (Binary Coded Decimal) 22
2.4.2 Codice GRAY (grigio) 23
2.4.3 Codice EBCDIC (Extended Binary Coded Decimal Interchange Code) 23
2.4.4 Codice ASCII (American Standard Code Information Interchange) 23
2.4.5 Codice ISO 8859 24
2.4.6 UNICODE 25
2.4.7 Implementazioni Unicode: UTF-8 26

3. Il livello MV0 – Macchina fisica (Hardware) 27


3.1 MV0.0 – cenni sui circuiti analogici 27
3.1.1 Legge di Ohm e Resistori 27
3.1.2 Condensatori 28
3.1.3 Induttori 28
3.2 MV0.1 – Circuiti digitali e porte logiche 30
3.2.1 Elementi di Algebra Booleana 31
3.2.2 Esempi di applicazione dell’Algebra Booleana ai circuiti 32
3.2.3 Operatori Universali NAND e NOR 33
3.2.4 Operatore OR esclusivo XOR 34
3.3 Funzioni Logiche 35
3.3.1 Mappe di Karnaugh 36
3.3.1.1 Esempi di sviluppi in mappe di Karnaugh 37
3.3.2 Da somme di prodotti a prodotti di somme 39
3.4 Circuiti combinatori Standard 39
3.4.1 Circuiti decodificatori e codificatori 39
3.4.2 Circuiti multiplexer e demultiplexer 41
3.4.3 Logiche programmabili (PLA) 42
3.5 Composizione di circuiti combinatori Standard 43
3.6 Circuiti Sequenziali 45
3.7 Circuiti Sequenziali Base: Latch (o flip-flop tipo latch) 46
3.7.1 Latch Set Reset (SR) 46
3.7.2 Flip-flop (improprio) Set Reset Triggered (SRT) 47
3.7.3 Flip-flop master-slave 47
3.7.4 Flip-flop D (Delay) 47
3.7.5 Flip-flop JK 48
3.8 Sintesi dei circuiti Sequenziali 49

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

4. Il livello MV1 – Struttura dei calcolatori (Firmware) 54


4.1 La macchina di Von Neumann 54
4.1.1 La Rete di Governo 55
4.1.2 Le Interruzioni 56
4.1.2.1 Commutazione del Contesto (Context-Switch) 57
4.1.2.2 Classificazione degli Interrupt 57
4.1.2.3 Interrupt multipli 58
4.2 La Memoria dei Computer 59
4.3 La Memoria di lavoro 61
4.3.1 RAM Statica (SRAM) 61
4.3.2 RAM Dinamica 61
4.3.3 ROM 62
4.4 Dispositivi di Input e Output 63
4.4.1 Accesso diretto alla Memoria (DMA) 65
4.4.2 Canali e processori di I/O 68
4.5 La Microprogrammazione 69

5. Il livello MV2: La macchina “Assembler” 70


5.1 Programming model 70
5.1.1 Le Risorse della Macchina MV2 70
5.1.1.1 I Registri 71
5.1.1.2 La Memoria 71
5.1.2 Linguaggio a livello MV2 - Istruzioni 72
5.1.3 Modalità di indirizzamento 73
5.1.3.1 OPERANDO EFFETTIVO o IMMEDIATO 73
5.1.3.2 INDIRIZZO ESPLICITO 73
5.1.3.3 INDIRIZZO IMPLICITO 75
5.1.4 Progettazione del set di Istruzioni ed Ortogonalità 75
5.1.5 Controllo flusso istruzioni 76
5.1.5.1 Le Procedure o Subroutine 76
5.1.5.2 Le Coroutine 77
5.1.5.3 Gestione dello stack - la notazione polacca inversa 77
5.1.5.4 Le procedure ricorsive – Gestione dell’heap 79

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

8. Architetture dei Computer Paralleli 111


8.1 Modelli Architetturali Paralleli 111
8.1.1 Multiprocessori o sistemi a memoria condivisa 111
8.1.2 Multicomputer o sistemi a memoria distribuita 112
8.1.3 Modelli ibridi 112
8.2 La struttura di interconnessione 113
8.2.1 Topologia delle reti 114
8.2.2 Switching e routing 115
8.3 Misura delle prestazioni (hardware) 117
8.4 Misura delle prestazioni complessive 118
8.4.1 Legge di Amdahl 118
8.5 Classificazione delle Architetture Parallele 120
8.5.1 SIMD – Array e Vector processor 121
8.5.2 MIMD – MultiProcessori a memoria condivisa 122
8.5.2.1 UMA a BUS 123
8.5.2.2 UMA a crossbar switch 124
8.5.2.3 UMA a switch multistrato 125
8.5.2.4 NUMA – Non Uniform Memory Access 125
8.5.2.5 COMA – Cache Only Memory Access 126
8.5.3 MIMD – Multicomputer a scambio di messaggi 127
8.5.3.1 MPP – Massive Parallel Processor 127
8.5.3.2 COW – Cluster of Workstation 128

3
1. Approccio strutturato alle architetture dei calcolatori

1.1 Introduzione

Elaboratore elettronico, sistema di elaborazione, calcolatore elettronico, cervello elettronico, macchina da


calcolo, (personal) computer (digitale), ordinateur, computador, … sono tanti i nomi (o sinonimi?) usati nel
linguaggio comune per indicare un “oggetto”, che è diventato ormai indispensabile nel mondo moderno.
Tutti questi nomi, non sono dei veri e propri “sinonimi” in quanto i sostantivi usati sono molto differenti fra
loro: tale differenza si fonda e testimonia che, cambiando punto di vista, cambia quello che si vede e quello
1
che si ottiene o si può ottenere da questo “oggetto” che, specializzandolo appena, potremmo anche
chiamare “macchina”.
Chi vede ed usa questa macchina come un’eccezionale e velocissimo strumento di calcolo, lo può chiamare,
a ragione, “calcolatore”, “computer”, “macchina da calcolo”. Chi invece non effettua calcoli ma utilizza la
macchina per registrare ed archiviare informazioni, potendole poi riprendere raggruppandole, riordinandole,
combinandole, lo vede come una macchina capace di trattare tali informazioni e quindi lo può chiamare, di
diritto, “elaboratore”, “sistema di elaborazione”, “ordinateur”. Chi, infine, utilizza tale macchina per avere dei
suggerimenti su cosa fare, su qual è la scelta migliore tra due o più alternative, allora ha la sensazione di
avere a disposizione un “collaboratore meccanico”, uno strumento “intelligente”, in grado di “ragionare” e
quindi non può che chiamarlo “Cervello elettronico”.
Nessuno di questi nomi è in grado di sintetizzare l’essenza, la specifica essenziale di tale macchina che
invece risulta corretta ed appropriata nella seguente definizione:

"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.

L’uomo che deve risolvere un problema, interagisce con l’elaboratore per:


a) programmarlo, ovvero indicare la sequenza di istruzioni che la macchina dovrà eseguire
b) inserire i dati ed ottenere i risultati ovvero prelevare dalla macchina la soluzione del suo problema.

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".

1.2 Strutturazione verticale a livelli di 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:

Figura 1.1 - Strutturazione a livelli di Macchine Virtuali

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.

Formalmente, ogni livello MVi è identificato e caratterizzato dalla coppia:


Ri = Insieme di risorse o oggetti visibili a chi programma quel livello.
Li = Linguaggio disponibile per il controllo e la gestione degli elementi dell'insieme Ri.

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).

Ogni Istruzione Primitiva o Meccanismo del linguaggio generalizzato Li è implementata da un PROGRAMMA


o POLITICA scritto nel linguaggio Lj dove, evidentemente:

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.

1.3 Richiami sulla COMPILAZIONE e 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

Figura 1.2 - Programmazione tramite compilazione pura

Per Interpretazione PURA, si intende l’operazione eseguita da un particolare programma (interprete) in


grado di leggere un programma sorgente, scritto in linguaggio di alto livello, trasformando ciascuna
istruzione direttamente nella corrispondente sequenza di istruzioni in linguaggio macchina. Il programma
interprete, che sarà in genere scritto in linguaggio di basso livello, rimane in macchina durante l'esecuzione
del programma fino al suo completamento.

Dati PROGRAMMA Risultati


SORGENTE
----------------------------------
----
FASEINTERPRETE
DI INTERPRETAZIONE
Figura 1.3 - Programmazione tramite interpretazione pura

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

b) Eliminazione errori sintattici in fase di a) Facilità nel trovare e correggere eventuali


compilazione errori semantici
c) Poter utilizzare linguaggi di alto livello b) Ridotto ingombro complessivo dei programmi
diversificati e “specializzati” per comporre in quanto tutti utilizzano le stesse primitive
un’unica procedura (procedura = insieme di dell’interprete.
programmi). c)
a) Per effettuare modifiche al programma è b) Velocità di esecuzione più bassa che nella
Svantaggi

necessario disporre del programma compilazione


sorgente, compilarlo e rilanciarlo. c) Presenza di possibili errori sintattici che si
b) Difficoltà nel trovare e correggere eventuali manifestano durante l’esecuzione.
errori

Tabella 1.1 - Vantaggi e Svantaggi tra Compilazione ed Interpretazione

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.

1.4 Livelli tipici di un Sistema di Elaborazione

I concetti introdotti precedentemente, ci consentono di affrontare lo studio delle architetture tramite un


approccio “verticale” ai sistemi di elaborazione.
Per fissare le idee, utilizziamo un esempio ed analizziamo in dettaglio quali sono i livelli tipici di un tipico
sistema di elaborazione:

Livello 4

Livello 3

Livello 2

Livello 1

Livello 0

Figura 1.4 - Un esempio di livelli logici di MV

Analizziamo in dettaglio i singoli livelli, a partire da quello più esterno:

MV4: Livello Applicativo


R4= Archivi, Data Base e altri dati strutturati
L4= Java/Cobol/Pascal/Basic/Assembler
I linguaggi L4 sono spesso chiamati "linguaggi di alto livello" indicando con questo la grande distanza che vi
è tra questi ed il livello logico digitale Mv0. Per passare ai livelli sottostanti si usando indifferentemente
interpreti (BASIC) o compilatori (C++) o configurazioni miste (Java)

MV3: Livello del SISTEMA OPERATIVO


R3= Memoria centrale e di massa, file, programmi, processori
L3= Linguaggio di programmazione concorrente/sequenziale con chiamate a primitive di sistema.
Inizia da qui la macchina dedicata ai "programmatori di sistema" ovvero per quegli utilizzatori specializzati
nella progettazione ed implementazione di nuove macchine virtuali. Per passare ai livelli sottostanti si utilizza
in genere un interprete (Sistema Operativo) che rende disponibili le primitive di sistema, anche chiamate
"Supervisor Call"

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"

MV1: Livello della MICROARCHITETTURA o “STRUTTURA”


R1= Registri, moduli funzionali, operatori aritmetico-logici (ALU)
L1= Linguaggio di microprogrammazione
A questo livello la macchina è in grado di eseguire semplici operazioni definite dal linguaggio L1. Se tra
queste semplici istruzioni sono incluse anche quelle necessarie per spostare valori tra registri e ALU, allora
avremo un vero e proprio microprogramma per ogni Istruzione macchina.
Il microprogramma può anche essere assente qualora i trasferimenti dati (o data path) tra registri e ALU
siano controllati direttamente da hardware.
Nelle macchine microprogrammate il linguaggio L1 viene usato per scomporre ulteriormente ed interpretare
le istruzioni del livello L2.

MV0: Livello LOGICO DIGITALE


R0= Porte logiche, Circuiti combinatori e sequenziali, collegamenti fisici, ..
L0= Non definito (Algebra di Boole)
E' questo il livello più basso dove incontriamo i componenti elettronici sotto forma di porte logiche.
Effettivamente tali porte saranno poi realizzate usando componenti discreti analogici come "transistor", ma
per gli scopi del progettista, conviene mascherare la natura "analogica" dei componenti per semplificare il
lavoro di analisi e progettazione.

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

Anche per MV3 si possono individuare i seguenti sottolivelli tipici:

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

1.5 L’equivalenza Hardware-Software


La strutturazione a livelli gerarchici di Macchine Virtuali ci consente di studiare i sistemi di elaborazione
senza essere eccessivamente condizionati dall’evoluzione della tecnologia. Già nei primi elaboratori, infatti,
era possibile individuare almeno due livelli: il livello ISA ed il livello digitale: il software (ovvero il programma)
veniva interpretato ed eseguito direttamente dall'Hardware (la macchina) costituita da registri e ALU.
Nel tempo, potendo aumentare la quantità di software in maniera più semplice rispetto all'hardware, si è
iniziato a mascherare delle "funzioni" tipiche dell'hardware (es. moltiplicazione) tramite algoritmi "software".
La distinzione tra hardware è software diventa sempre più irrilevante tanto che oggi possiamo affermare che:
3
"Hardware e Software sono logicamente equivalenti"

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.

Successivamente, negli anni 60 e 70 le dimensioni dei microprogrammi crebbero ancora in maniera


consistente, per realizzare sistemi in grado si eseguire istruzioni sempre più complesse (CISC) fino a che,
verso la fine degli anni '70, ci si rese conto che tale aumento, oltre che rendere eccessivamente complessa
la progettazione e realizzazione dei sistemi, si correva anche il rischio di un effettivo rallentamento del
sistema stesso.

Parallelamente all'evoluzione della microprogrammazione, si evolveva anche il livello MV3 (Sistema


Operativo). Tale livello è stato introdotto principalmente per ottimizzare i tempi morti tra l'esecuzione di un
JOB (compilazione programma, esecuzione programma, dump della memoria, …) ed un altro.
Le funzioni del S.O. crebbero nel tempo per la gestione ottimizzata della CPU, della memoria e delle
periferiche.

1.6 Strutturazione Orizzontale a MODULI nei livelli - generalità


Per ogni livello MVi, sono note le funzionalità espletate in quel livello, se quindi indichiamo con:

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 :

SIi = (MUi , FIi)


con
MUi = { Mi1, Mi2, .... Min} = insieme dei MODULI di ELABORAZIONE ad ognuno dei quali e' affidata
l'elaborazione di una Fij.
FIj = STRUTTURA DI INTERAZIONE, mediante la quale i moduli Mij cooperano per l'espletamento di tutte le
funzioni di Mvi.

Mi1
Mi2

STRUTTURA
DI
INTERAZION
E Mij

Fig. 1.5.1

Ogni MODULO di ELABORAZIONE è definito come una entità computazionale AUTONOMA E


SEQUENZIALE.
La caratteristica di AUTONOMIA, permette la progettazione dei singoli moduli indipendentemente dagli altri.
La caratteristica di SEQUENZIALITA', assicura la presenza di un singolo LUOGO DEL CONTROLLO,
ovvero l'attività del modulo, può essere descritta da una lista sequenziale di istruzioni, detta appunto
"algoritmo di controllo". Questa sequenzialità, comunque non preclude la possibilità che possano esserci
delle istruzioni eseguibili simultaneamente. In ogni caso, conviene riferirci al parallelismo che si viene a
creare tra i vari moduli di elaborazioni, che caratterizza la sua strutturazione.

Esempi di “moduli di elaborazione” presenti ai livelli tipici sono:

LIVELLO MV1 = UNITA' DI ELABORAZIONE


LIVELLO MV3 = PROCESSI

dove possiamo individuare i seguenti “algoritmi di controllo” saranno:

LIVELLO MV1 = STRUTTURA DELLE MICROISTRUZIONI


LIVELLO MV3 = STRUTTURA DEI COMANDI SECONDO LINGUAGGI DI PROGRAMMAZIONE
CONCORRENTE

Le unità di elaborazione, sono implementate tipicamente secondi gli schemi seguenti:

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:

RETI COMBINATORIE DI CALCOLO


RETI DI INSTRADAMENTO DATI
REGISTRI INTERNI
REGISTRI DI INTERFACCIAMENTO CON L'ESTERNO

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).

Il funzionamento di una LLC e' il seguente:


a) ad ogni impulso di clock (generalmente si sceglie il fronte di discesa), il valore delle variabili di stato
successivo viene immagazzinato nelle memorie per costituire il nuovo stato.
b) la parte combinatoria della rete, in base ai valori degli ingressi e dello stato, fornisce (in forma stabile
dopo un certo tempo T) i valori delle uscite, alcune delle quali destinate a formare lo stato successivo.
c) Il cuore del funzionamento e' il "buffer" che fornisce l'elemento di "STATICIZZAZIONE" e "STA-
BILIZZAZIONE" a tutta la rete. Il periodo degli impulsi di clock è condizionato dal massimo tempo T che
occorre alla rete combinatoria, per fornire valori stabili alle uscite.

Gli AUTOMI (Macchine sequenziali sincrone) possono essere realizzati secondo due diversi modelli
matematici:

MODELLO DI MEALY:

𝑌(𝑡) = 𝐹𝑦 [𝑋(𝑡), 𝑆(𝑡)] 𝐹𝑦 = 𝐹𝑢𝑛𝑧𝑖𝑜𝑛𝑒 𝑑𝑖 𝑈𝑠𝑐𝑖𝑡𝑎


{
𝑆(𝑡 + 1) = 𝐹𝑠 [(𝑋(𝑡), 𝑆(𝑡)] 𝐹𝑠 = 𝐹𝑢𝑛𝑧𝑖𝑜𝑛𝑒 𝑑𝑖 𝑆𝑡𝑎𝑡𝑜

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.

Per la realizzazione delle unità di elaborazioni sono possibili le seguenti combinazioni:


PC Mealy Moore Moore
PO Moore Moore Mealy

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

2.1 Richiami sul sistema di numerazione binario e conversione di base


Il Sistema di numerazione BINARIO, così come quello DECIMALE è di tipo posizionale . La cifra 1, assume
il valore di potenze del 2 via via crescenti, man mano che la posizione si sposta verso sinistra, come
succede nel sistema decimale, dove le potenze sono quelle del 10.
14
Per convertire un numero da BINARIO a DECIMALE, basta dare il giusto peso a ciascuna cifra.
Ad esempio, il numero binario 10111, in decimale diventa:
4 3 2 1 0
1*2 + 0*2 + 1*2 + 1*2 +1*2 = 23

Per convertire da DECIMALE a BINARIO, possiamo usare due metodi:

- 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

- Metodo delle divisioni successive


Tenendo presente la rappresentazione posizionale, possiamo dividere il numero per 2 e quindi riportare il
resto della divisione come cifra binaria, a partire da destra. Il quoziente verrà diviso successivamente fino ad
ottenere il quoziente 0.

Ad esempio, per trasformare in binario il numero 237, si eseguono le divisioni successive:


237 2

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:

27 + 11011 + Il procedimento è molto semplice: quando la somma è > 1, ovvero si ottiene


23 = 10111 = un risultato a due cifre, occorre scrivere la cifra a destra e riportare 1. In
decimale il procedimento è analogo: quando la somma è maggiore di 9,
50 110010 ovvero si ottiene un risultato a due cifre, occorre scrivere la cifra a destra e
riportare 1.

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

9* 1001 * La moltiplicazione viene effettuata incolonnando opportunamente le


3= 11 = moltiplicazioni parziali e quindi effettuando la somma.
27 1001
1001

11011

59 3 111011 11 Così come per la moltiplicazione, la divisione si esegue “abbassando” il


dividendo succesivamente e calcolando i resti con delle sottrazioni
29 19 00101 10011 successive. Ogni volta che si “abbassa” una cifra ed il numero è minore del
2 101 divisore, occorre accostare uno 0 al risultato.
10

2.1.1 Strutture fisiche GERARCHICHE di INFORMAZIONE

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:

BIT Informazione elementare a 2 valori (0/1 , SI/NO)


HALF BYTE o NIBBLE Gruppo di 4 BIT, serve per individuare un’ informazione a 16 valori
BYTE Gruppo di 8 BIT (ottetto), serve per individuare un’informazione a 256 valori
WORD (PAROLA) Unità fondamentale di informazione usata per i trasferimenti.

Nell’uso comune, sono state individuate le seguenti strutture ulteriori, basate sul numero di valori possibili
immagazzinabili:

WORD Gruppo di 16 BIT: individua 65.536 possibili valori


DOUBLE WORD Gruppo di 32 BIT: individua 4.294.967.296 possibili valori
QUAD WORD Gruppo di 64 BIT: individua 1,844674407371e+19 possibili valori

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:

BIT Informazione elementare a 2 valori (0/1 , SI/NO)


NIBBLE Gruppo di 4 BIT
BYTE Gruppo di 8 BIT o di 2 NIBBLE
WORD Gruppo di 16 BIT o di 4 NIBBLE o di 2 BYTE
DOUBLE WORD Gruppo di 32 BIT o di 8 NIBBLE o di 4 BYTE o di 2 WORD
QUAD WORD Gruppo di 64 BIT o di 16 NIBBLE o di 8 BYTE o di 4 WORD o di 2 DOUBLE WORD

Utilizzando le strutture fisiche “NIBBLE”, possiamo introdurre un sistema di numerazione strettamente


correlato a quello binario ovvero che mantiene intatto il contenuto informativo della base: il sistema
esadecimale, a base16, estendendo le classiche cifre da 0 a 9 impiegate per il sistema decimale con le
lettere da “A” a “F”, per rappresentare le combinazioni binarie da 1010 a 1111.
Nelle architetture dove avevano un significato pratico usare le strutture fisiche a 3 bit, veniva utilizzato il

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.

2.1.2 Rappresentazione binaria dei numeri relativi


Nei richiami sulle operazioni tra numeri binari, abbiamo solo preso in considerazione l'insieme costituito dai
numeri naturali, normalmente impiegato negli algoritmi non aritmetici (logici o gestionali).
Per poter effettuare dei calcoli aritmetici, dovremo estendere i discorsi sopra esposti al caso più generale dei
numeri relativi e per far questo è indispensabile definire preliminarmente la dimensione della struttura fisica
dell’informazione.
Ad esempio, per effettuare delle sottrazioni tra numeri, e quindi poter rappresentare il numero negativo
introducendo l'informazione "segno", dobbiamo prevedere di riservare lo spazio (un bit almeno) per tale
informazione. La rappresentazione di un numero relativo può quindi essere fatta secondo due diverse
modalità: modulo e segno e complemento alla base.

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”.

b) COMPLEMENTO ALLA BASE


In questo modalità, i numeri positivi, zero compreso occupano la prima metà delle possibili combinazioni con
la struttura scelta da n bit, mentre i numeri negativi, occupano la seconda metà. Riportiamo la seguente
tabella come esempio:

-128..+127 Dec = 80..7F Hex = 200..177 Oct -> 8 bit


-256..+256 Dec = 100..0FF Hex = 400..377 Oct -> 9 bit
-512..+511 Dec = 200..1FF Hex = 1000..0777 Oct ->10 bit
-2048..+2047 Dec = 800..7FF Hex = 4000..3777 Oct ->12 bit
-32768..+32767 Dec = 8000..7FFF Hex = 100000..077777 Oct ->16 bit

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).

b1) Gestire gli overflow


La rappresentazione unsigned consente di individuare rapidamente eventuali errori di overflow analizzando
semplicemente il riporto, chiamato “Carry” dal bit più significativo della struttura fisica.
La rappresentazione in modulo e segno, va analizzata, sia per il Carry che per l’eventuale riporto sul bit del
segno (bit più significativo).
Ad esempio, considerando strutture da 8 bit in complemento a due analizziamo le seguenti casistiche:
a) 34-43 (decimale)  0 0100010 +
1 1010101 =
--------------------
1 1110111  - 9 (decimale) corretta, nessun riporto

b) 66-43 (decimale)  0 1000010 +


1 1010101 =
--------------------
1 0 0010111  23 (decimale) corretta, ho riporto su segno e Carry

c) 34+117 (decimale)  0 0100010 +


0 1110101 =
--------------------
1 0010111  151 (decimale) errore, ho un riporto solo su bit del segno

d) -105-95 (decimale)  1 0010111 +


1 0100001 =
--------------------
1 0 0111000  -200 (decimale) errore, ho un riporto solo su Carry

Da tali esempi si deduce una regola generale:


a) Non si ha overflow se si sommano numeri con segno discorde ovvero si sottraggono numeri con
segno concorde;
b) Si ha overflow se si ottiene un numero con segno diverso da quello atteso ovvero:
 se sommando due positivi si ottiene un numero negativo oppure se sommando due negativi
si ottiene un numero positivo
 se sottraendo un negativo da un positivo si ottiene un negativo oppure se sottraendo un
positivo da un negativo si ottiene un positivo
Nella rappresentazione in complemento a 2 avremo quindi, indicando con S(x) il bit del segno dell’operando
x, avremo oVerflow in questi casi:

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)

2.1.3 Rappresentazione binaria dei numeri reali


Per completare l'insieme dei numeri reali, occorre dare una rappresentazione anche ai numeri razionali.
Anche in questo caso sono possibili più soluzioni ma, per i nostri scopi consideriamo solo la
rappresentazione in VIRGOLA MOBILE, che consiste nell'individuare ogni numero tramite la coppia (e,m)
"esponente" e "mantissa", tali che:

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ù.

Ad esempio, potremo avere i seguenti dimensionamenti:

E M totale errore err.circa


SINGOLA PRECISIONE: 8 bit | 23 bit | 32 bit | 2^(-23) | 10^(-7)
DOPPIA PRECISIONE : 8 bit | 55 bit | 64 bit | 2^(-55) | 10^(-17)

Esempio:
Per rappresentare il numero 10111,0011b, occorre prima normalizzarlo in 0,101110011b * 2^5 e quindi
avremo, in singola precisione:

S Esponente Modulo mantissa


binario 0 0000 0101 000 0000 0000 0001 0111 0011
Hexadec 0 05 000173

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

Lo standard prevede le seguenti distribuzione dei bit per ciascun campo:


Nome Formato Nome in C Nome in f90 Totale S E M Cifre Decimali
Single float real*4 32 1 8 23 7
Double double real*8 64 1 11 52 16
Extended* long double? real*10+ 40+ 1 15+ 63+ 19+
Quadruple* long double??? real*16 128 1 15 112 34
* Formati opzionali, dipendenti dal linguaggio o dal compilatore.

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.

Riassumendo, lo standard IEEE-754 stabilisce i seguenti valori limite:

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

2.2 Organizzazione delle memorie

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.
A titolo di esempio vediamo 3 modi diversi è possibile organizzare una memoria da 96 bit total
Sembrerebbe che vi sia una grande libertà nel raggruppare i bit in celle di memoria. In effetti, nel passato si
sono avute le seguenti scelte progettuali:

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.

2.4 I codici di caratteri


La necessità di rappresentare completamente l'informazione, porta ad estendere i concetti di
rappresentazione numerica esposti precedentemente verso l'ALFABETO usato normalmente dall'uomo per
comunicare. Questo alfabeto viene denominato come ALFABETO ESTERNO ed è costituito dalle 26 lettere
maiuscole e minuscole che compongono l'alfabeto inglese, dalle 10 cifre decimali e dai vari segni di
interpunzione. Non è possibile una conversione diretta tra i simboli dell'alfabeto esterno e i simboli dell'ALFA-
BETO INTERNO, ovvero dell'alfabeto usato dal calcolatore in quanto quest'ultimo è dotato dei due soli
simboli 0 e 1. Per poter rappresentare l'alfabeto esterno, dovremo raggruppare più bit in modo da formare
delle combinazioni che abbiano un significato.

21
Chiamando queste combinazioni PAROLE DEL CODICE, possiamo formalizzare le seguenti definizioni:

CODICE: un particolare modo di comunicare secondo simboli e regole precise.

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):

n >= m con m = intero non minore di log2 N

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.

2.4.1 Codice BCD (Binary Coded Decimal)

è 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).

2.4.2 Codice GRAY (grigio)

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

2.4.3 Codice EBCDIC (Extended Binary Coded Decimal Interchange Code)

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.

2.4.4 Codice ASCII (American Standard Code Information Interchange)

É 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".

2.4.5 Codice ISO 8859

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:

•C0 Controls and Basic Latin •CJK Ideographs


•C1 Controls and Latin-1 Supplement •Hangul Syllables
•Latin Extended-A •Latin Extended-B •High Surrogates
•IPA Extensions •Spacing Modifier Letters •High Private Use Surrogates
•Combining Diacritical Marks •Low Surrogates
•Greek •Cyrillic •Private Use Area
•Armenian •CJK Compatibility Ideographs
•Hebrew •Alphabetic Presentation Forms
•Arabic •Arabic Presentation Forms-A
•Devanagari •Combining Half Marks
•Bengali •CJK Compatibility Forms
•Gurmukhi •Small Form Variants
•Gujarati •Arabic Presentation Forms-B
•Oriya •Halfwidth and Fullwidth Forms
•Tamil •Specials
•Telugu
•Kannada
•Malayalam
•Thai
•Lao
•Tibetan
•Georgian
•Control Pictures
•Optical Character Recognition
•Enclosed Alphanumerics
•Box Drawing
•Block Elements
•Geometric Shapes
•Miscellaneous Symbols
•Dingbats
•CJK Symbols and Punctuation
•Hiragana •Katakana
25
•Bopomofo
•Hangul Compatibility Jamo
•Kanbun
•Enclosed CJK Letters and Months
•CJK Compatibility

2.4.7 Implementazioni Unicode: UTF-8


L’Unicode Transformation Format-8 (UUT-8) è un’implementazione di UNICODE che cerca di superare il
costo imposto dalla codifica UNICODE a 32 bit, che ne frenerebbe l’applicazione, specialmente quando il
charset è simile ad ASCII, come nel caso di ISO 8859, usando i seguenti accorgimenti:
 Eliminazione dei byte “null” prevedendo una codifica a lunghezza variabile:
 Se il bit più significativo del codice è = 0, allora individua una codifica ASCII a 7 bit ovvero il “C0
Control and Basic Latin” da U+0000 a U+007F
 Altrimenti, se 1, si analizza il bit successivo e:
o Se = 0, allora è un byte successivo: per decodificare occorre ripartire da primo byte
o Se = 1, allora è il primo byte di una sequenza di N byte, dove N è pari al numero di bit “1”
consecutivi, a partire dal bit più significativo.

La seguente tabella riepiloga tale implementazione:

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/

3.1.1 Legge di Ohm e Resistori

Consideriamo il circuito a fianco dove un generatore di tensione (una


1

I pila ad esempio) è connesso ad un resistore tra i punti 1 e 2. Il


campo elettrico fornito dal generatore di tensione è di 5 Volt ed è
5Vol t 1KO hm
orientato con il positivo nel punto 1. Il simbolo connesso al punto 2
rappresenta una connessione a potenziale 0 da ussare quindi come
2

riferimento. La legge di Ohm regola la relazione tra corrente (intesa


come flusso di cariche positive nell'unità di tempo) e tensione (intesa
come manifestazione del campo elettrico) secondo la formula:
V = R I
Ovvero:
Tensione = Resistenza  Corrente
Secondo le seguenti unità di misura:
Volt = Ohm  Ampère
Nel caso in esempio, avremo:
I = V/R = 5/1000 = 0,005 A R = V/I = 5/0,005 = 1000  V = RI = 10000,005 = 5
V

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

generatore di tensione e da due resistori di uguale valore. Fermo


1K restando che la tensione tra i punti 1 e 2 è sempre di 5 V (per
5V 1K definizione di generatore di tensione), vogliamo sapere la tensione
ai capi di ciascuna resistenza.
Per questo, prima ricaviamo la corrente: I = V/R = 5/2000 = 2,5 mA
2

Quindi riapplichiamo la legge di Ohm alle singole resistenza


trovando:
V = RI = 10000,0025 = 2,5 V

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

0 a infinito e quindi la tensione ai


capi dell’armature varia da 0 a V
secondo la seguente formuila
V C esponenziale diagramata a destra:
-t/RC
63%
Vc = V * (1 - e )
Applicando la legge di Ohm
possiamo ricavare anche il valore
2

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).

3.2.1 Elementi di Algebra Booleana


 negazione (NOT)
si indica con il segno - sopra l’operando, ad esempio: 1  0 0 1
 prodotto logico (AND)
si indica con il segno . tra i due operandi, ad esempio: 0  0  0  1  1  0  0 11  1
 somma logica (OR)
si indica con il segno + tra i due operandi, ad esempio: 1  1  1  0  0  1  1 000
Al posto dei valori 1 o 0, è possibile utilizzare delle variabili booleane ovvero delle variabili che possono
assumere solo i due valori 0 o 1. Valgono le seguenti proprietà dirette (basta sostituire i valori 0 o 1 e
verificarne la veridicità):
x 11 x  0  x x  x 1 xxx
x  0  0 x 1  x xx 0 x
x
x
idempotenza
Valgono inoltre:
Proprietà commutativa:
x y yx x y  yx
Proprietà associativa:
x   y  z   x  y  z  x  y  z x   y  z   x  y  z  x  y  z
Proprietà distributiva:
 x  y   x  z  x   y  z  x  y   x  z  x   y  z
Teoremi dell’assorbimento:
x   x  y  x x   x  y  x
x   x  y  x  y x   x  y  x  y
Teorema di De Morgan:
x y  yx x y  y  x

Quest’ultimo teorema è di fondamentale importanza in quanto ci permette di esprimere l’operatore OR in


funzione degli operatori AND e NOT e, viceversa, l’operatore AND in funzione degli operatori OR e NOT.
Le proprietà precedenti ci permettono di stabilire il principio di dualità dell’algebra booleana: ogni identità
ed ogni proprietà dell’algebra booleana resta valida se si scambiano tra loro gli operatori AND e OR e gli
elementi 1 e 0.

31
3.2.2 Esempi di applicazione dell’Algebra Booleana ai circuiti
I seguenti due circuiti sono equivalenti applicando la proprietà distribuitiva

I seguenti 4 circuiti equivalenti si ricavano applicando direttamente il teorema di De Morgan

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/xx xxx
( s om ma ) x / y  x  y  x  y ( prodotto) x y x y x y

I corrispondenti circuiti logici sono:


Negazione:

Somma:

Prodotto:

33
3.2.4 Operatore OR esclusivo XOR

Questo operatore, detto anche “somma modulo 2” o “anticoincidenza”, si indica con il


simbolo  e realizza la seguente tabella di verità (nel caso di due variabili):

x1 x2 x1  x2
0 0 0
0 1 1
1 0 1
1 1 0

l’espressione analitica corrispondente è quindi:


  
x1  x 2  x1 x 2  x1 x 2  x1  x 2  x1  x 2
L’operatore non è universale in quanto, da solo non è in grado di realizzare le operazioni
di somma e prodotto.
La negazione della funzione XOR, x1  x 2 controlla l’identità delle variabili alle quali è
applicato l’operatore e viene spesso indicata con x1  x 2 . L’operatore  si chiama
“coincidenza” o semplicemente “XAND” o “XNOR” o “NXOR”.

x1 x2 x1 x2
0 0 1
0 1 0
1 0 0
1 1 1

Possibili implementazioni dello XOR tramite porte elementari:

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

Le forme canoniche viste, pur risultando matematicamente corrette, necessitano di un procedimento di


semplificazione che porti ad una espressione più sintetica possibile e quindi ad una realizzazione meno
costosa.

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:

Mappa per 2 variabili Mappa per 3 variabili

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

Mappa per 4 variabili

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

L’utilizzo per più di 4 variabili è possibile ma risulta piuttosto laborioso.


Le mappe di Karnaugh permettono di determinare, a partire da una forma normale in somma di prodotti, i
mintermini adiacenti ovvero quelli che possono essere semplificati. I gruppi possono essere costituiti da 2, 4
o 8 insiemi. Le mappe sono da vedersi come sferiche, ovvero i mintermini dell’ultima colonna sono adiacenti
a quelli della prima colonna e quelli dell’ultima riga adiacenti a quelli della prima riga.
Facendo riferimento alla mappa per 4 variabili, ad esempio, è agevole riconoscere che il mintermine
x1 x 2 x 3 x 4 è adiacente, sia a x1 x 2 x 3 x 4 che a x1 x 2 x 3 x 4 . Si ha infatti:
x1 x 2 x 3 x 4  x1 x 2 x 3 x 4  x 2 x 3 x 4  x1  x1   x 2 x 3 x 4
x1 x 2 x 3 x 4  x1 x 2 x 3 x 4  x1 x 2 x 4  x 3  x 3   x1 x 2 x 4

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

Consideriamo la seguente funzione:

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

Si ricava la seguente Mappa di Karnaugh:

x1 x2
00 01 11 10
x 3 x4
(0) (4) (12) (8)
00 1 1

(1) (5) (13) (9)


01 1 1 1

(3) (7) (15) (11)


11 1 1 1

(2) (6) (14) (10)


10 1 1

dalla quale si ottiene: y  x1 x 2  x2 x 4 ed applicando il teorema di De Morgan:


y  y  x1 x 2  x2 x 4   x1  x 2    x2  x 4 
n-1
Da notare che questo metodo è conveniente nei casi in cui il numero di 1 della funzione sia superiore a 2 ,
con n = numero delle variabili.

3.4 Circuiti combinatori Standard

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:

3.4.1 Circuiti decodificatori e codificatori

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)

Disposizione dei pin Tabella della verità

Schema logico di funzionamento

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.

Esempio: 74148: 8 to 3 line encoder (codificatore decimale-binario)

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.

Tali problemi vengono risolti rispettivamente dai circuiti multiplexer e demultiplexer:

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).

Da un punto di vista pratico i demultiplexer sono integrati nei


decoder. Ad esempio, riguardando la tabella della verità del
74139, visto nel paragrafo 5.4.1, possiamo immaginare che
l'ingresso sia il pin G mentre gli input A e B selezionano il
canale su cui inviare il segnale G.

3.4.3 Logiche programmabili (PLA)


Le “Programmable Logic Array
“(PLA), sono circuiti programmabili che
implementano una funzione logica
descritta tramite somme di prodotti.
Nella figura a destra viene illustrato un
esempio di PLA composto da 12
ingressi e 6 uscite. Ciascuno dei 12
ingressi viene connesso ad un inverter
e quindi i 12 x 2 ingressi complessivi
vengono collegati, tramite fusibili
programmabili a 50 porte
logiche AND, ciascuna da 24 ingressi.
Le uscite delle 50 AND vengono poi
connesse, tramite fusibili
programmabili a 6 porte OR, ciascuna
da 50 ingressi che producono le uscite
delle 6 funzioni logiche definibili.
La programmazione consiste quindi
nel definire, a partire dai mintermini,
quali fusibili “bruciare” in uno schema
costituito dalle due matrici di ingresso
(24 x 50) e di uscita (50 x 6).

42
3.5 Composizione di circuiti combinatori Standard

Il comparatore è un circuito che


confronta artitmeticamente due valori
interi e restituisce 1 quando i due valori
sono deversi tra loro.
Nella figura a destra, viene mostrata la
soluzione tramite porte xor che
comparano bit a bit i due valori A e B.

Half Adder e Full Adder da un BIT


Introdotti all’inizio del paragrafo 5.2
come esercizio, sono i componenti alla
base delle ALU di una CPU ovvero
delle unità funzionali rese disponibili
direttamente al livello MV1.
Il Full Adder consente di effettuare la
somma di due bit, comprensiva del bit
di riporto precedente, producendo il
risultato ed il bit di riporto.
Si lascia al lettore la verifica del
funzionamento costruendo le relative
tabelle di verità
Full Adder/Subtractor da n bit
Componendo più full adder da un bit,
si possono ottenere dei
sommatori/sottrattori da n bit.
Nella figura a destra viene mostrato un
esempio a 4 bit.
Tramite l’ingresso “Control” è possibile
effettuare il Complemento a 2
dell’operando B (invertire tutti i bit e
sommare 1 ovvero ponendo a 1 il bit di
riporto del full-adder di ordine 0) e
quindi operare una sottrazione A – B.
Da rilevare che il collegamento seriale
dei riporti, introduce un ritardo
aggiuntivo “sistematico”

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.

Per migliorare il modello di un circuito sequenziale, introduciamo la definizione di stato ovvero la


configurazione della memoria del circuito ad un certo istante di tempo t.
Il Modello di un circuito sequenziale può essere quindi riscritto come:

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)

Le equazioni precedenti vengono anche dette equazioni interne del circuito.


La variabile "tempo" è nascosta nella definizione di S' che indica appunto lo stato all'istante successivo.
Rimane a questo punto stabilire come deve essere determinato l'istante successivo. In base proprio a
questa scelta avremo:

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

O meglio secondo i due modelli di Mealy e Moore:

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 )

3.7 Circuiti Sequenziali Base: Latch (o flip-flop tipo latch)


E' un dispositivo bistabile ovvero rimane nello stato in cui si trova finché un comando esterno non ne
provoca la commutazione ovvero il passaggio allo stato opposto. Normalmente un flip-flop dispone di due
terminali di uscita complementari, Q e Q; Q indica anche lo stato del dispositivo. I flip-flop sincroni sono
caratterizzati anche da un ingresso di clock dove può essere applicato un segnale che controlla la
commutazione in corrispondenza del fronte di salita o, come normalmente avviene, di discesa.

3.7.1 Latch Set Reset (SR)

SIMBOLO S R Q

0 0 Non varia
Q
S
_ 0 1 0
1 0 1
R Q
1 1 non ammessa

Il circuito è asincrono e si comporta come un “bistabile”:


se entrambi gli ingressi sono 0, l’uscita non cambia ovvero
rimane stabile l’ultima configurazione assunta.
Se supponiamo che tale configurazione sia Q=0, vediamo
che, se l’ingresso S viene messo a 1, allora l’uscita della NOR
in basso (Q) viene posta a 0 e quindi, essendo entrambi gli
ingressi della porta NOR in alto ora uguali a 0, allora l’uscita
(Q) commuta allo stato 1 e così rimane anche se S torna a 0.
Viceversa, quando l’ingresso R viene messo a 1, allora
l’uscita Q va a 1 e quindi la porta NOR in basso (Q) commuta
a 0 cosi’ rimane anche se R torna a 0.
Il diagramma temporale a fianco indica le dicerse transizioni,
tenendo conto dei tempi di ritardo Δt introdotti dalle porta NOT
nella propagazione del segnale tra ingresso ed uscita.
La configurazione S=1 e R=1 non è ammessa ovvero, se accade, il
circuito ha entrambe le uscite = 1 ed entrambe “instabili”, finché uno
dei due ingressi va a 0 e quindi si rientra nelle configurazioni
ammesse.
Il Latch SR può essere realizzato, sfruttando il teorema di De Morgan,
anche per ingressi a logica negata, usando NAND al posto delle NOR.
In questo caso la configurazione non nammessa sarà S’=0 e R’=0
46
3.7.2 Flip-flop (improprio) Set Reset Triggered (SRT)
SIMBOLO C S R Q
0 X X Non varia
S  0 0 Non varia
Q Q
S S
C  0 1 0
C _ _  1 0 1
R Q R Q
R
 1 1 non ammessa
Inserendo delle porte AND, si garantisce che entrambi gli ingressi S e R rimangano a 0 finché non viene
attivato il segnale “C” che possiamo quindi assumere come segnale di “clock”. In quanto modo, il circuita
diventa sincrono, con il difetto di campionare gli ingressi S e R, non solo durante la transizione da 0 a 1 del
segnale di clock ma anche durante tutto il periodo in cui il segnale rimane alto.
Per superare tale difetto, e quindi poter usare tale circuito per implementare
Fy secondo il modello di Moore - indispensabile per realizzare le unità di
elaborazione - possiamo inserire il circuito indicato a destra che trasforma un Uscita impulsi
segnale di clock simmetrico, come quello indicato in basso a destra, in
segnale ad “impulsi” di durata Δt (tempo progragazione porta NOT), come
riportato sempre a destra, appena sopra il segnale di ingresso. Ingresso clk
Rimane un possibile problema derivante dalla breve durate dell’impulso che, in presenza di disturbi esterni,
potrebbe anche non garantire la commutazione delle porte.

3.7.3 Flip-flop master-slave


SIMBOLO C S R Qn+1
Q Q
S _ C S Q 0 X X Qn
C _ S  0 0 Qn
Q C
R R Q _
Q  0 1 0
R
 1 0 1

 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.

3.7.4 Flip-flop D (Delay)


SIMBOLO C D Qn+1
Q 0 X Qn
S Q
C _  0 0
D
R Q C _  1 1
Q

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)

Anche questo componente può essere usato epr


realizzare reti sequenziali, tenendo presente il 0x/0 x0/1
1x/1
diagramma degli stati a fianco che prevede due
ingressi e che quindi potrebbe risultare più
Q=0 Q=1
conveniente in particolari situazioni. Nel diagramma x1/0
gli ingressi sono indicati nell’ordine JK ed il valore
“x” sta per 0 o 1, indifferentemente.
La versione “latch” del FF J-K che0 potrebbe essere così rappresentata (vedi libro di testo):
Non è in grado di eseguire la funzionalitè “toggle”
qualora il clock non sia di tipo “impulsivo”. Pe rtutto il
tempo in cui Clock rimane a 1, si avrà una
variazione continua delle uscite da 0 a 1 e viceversa
con un perido pari al tempo di transizione delle porte
AND e NOR.

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:

3.8.1 Progetto di un contatore sincrono modulo N con FF J-K


Il seguente esempio, mostra come utilizzare le mappe di Karnaugh per sintetizzare i circuiti sequenziali.
Supponiamo si voglia realizzare un contatore modulo 5 ovvero produca un impulso di uscita dopo 5 impulsi
in ingresso.

Il diagramma degli stati sarà del tipo:


Stato I_0 I_1 M5 OUT
1/0 1/0 1/0 1/0
0 1 2 3 4 S0 S0 S1 0 000

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

J1= q2\q1q0 00 01 11 10 K1= q2\q1q0 00 01 11 10


q0 0 1 x x q0 0 x x 1
1 x x x 1 x x x x

J0= q2\q1q0 00 01 11 10 K0= q2\q1q0 00 01 11 10


q2' 0 1 x x 1 1 0 x 1 1 x
1 x x x 1 x 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

D2 q2\q1q0 00 01 11 10 Dal confronto, possiamo osservare come l’utilizzo di


FF J-K comporta, in genere, un risparmio nella parte
q1q0 0 1 “combinatoria” ma rende più complessa la fase di
1 x x x analisi, dovendo operare con il doppio di ingressi.

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.

3.9.1 Il processo produttivo di un Circuito Integrato


Analizziamo sommariamente il processo produttivo di un circuito integrato per mettere in evidenza gli
elementi che contribuiscono a determinarne il costo.
Un circuito integrato si presenta esternamente secondi una delle forme tipiche della figura seguente:

Figura 3.1 – Esempi di contenitori di circuiti integrati

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):

1) Si inizia con un lingotto di cristallo di silicio altamente puro (chiamato


"99.999999999% - undici-nove"), ad esempio di 20 cm di diametro (se ne
producono anche di diverse dimensioni, da 15 a30 cm) e lungo 30 cm che viene
ridotto in fette, chiamate “wafer”, spesse circa 1 mm, proprio come si fa con
una mortadella, ma usando un’affettatrice diversa da quella usata dal salumiere.
Poiché i circuiti integrati sono molto piccoli, si potranno ricavare molti chip
(dies) in ciascuna fetta, come evidenziato nella figura a lato.
2) Il silicio è un semiconduttore e quindi viene trattato chimicamente (drogato),
tramite delle tecniche di fotoincisione, per definire le aree che costituiranno Gate
transistor, i conduttori e gli altri componenti passivi. Le dimensioni di queste Sourc Drain
aree sono estremamente piccole, inferiori al micrometro. La figura a lato mostra e
la sezione di un die dove è stati realizzato un transistor CMOS.
3) Purtroppo ogni wafer, nonostante la purezza del cristallo, presenta
comunque dei difetti puntuali di fabbricazione (dislocazioni) per cui ogni singolo .
die deve essere testato per l’effettivo funzionamento. In questa fase deve .
essere considerata la “resa” della produzione che sarà più alta quanto più .
piccoli saranno i singoli die. La figura a lato mostra tre die difettosi

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:

(numero die periferici) = (circonferenza del wafer) / (diagonale del die)

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:

(numero die periferici) = 62,83185 / 1,41421 = 44,42894


( numero die per wafer) = 314,1593 - 44,42894 = 269

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:

(resa del wafer) * (1 + (densità difetti) * (area die) / (alfa) ) ^ (-alfa)

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)

In definitiva avremo i seguenti volumi producibili:

1) die da 1.5x1.5 cm : (110) * (0.244) = 27


2) die da 1x1 cm: (269) * (0.492) = 132

Se il costo del wafer è di 3500 $ avremo i seguenti costi per die:

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)

4.1 La macchina di Von Neumann


Nonostante l’evoluzione tecnologica abbia fatto passi da gigante, i moderni computer sono ancora
organizzati, secondo il modello proposto da John Von Neumann nel 1946. L’importanza di del modello di
Von Neumann sta nella sua estrema sintesi grafica e nel fatto, partendo da tale modello, i sistemi di
elaborazione che seguirono misero un po’ d’ordine nel “groviglio” di fili contornavano minacciosamente le
apparecchiare e servivano (vedi “ENIAC”) per collegare i diversi componenti (processori, memoria, I/O).
I sistemi di elaborazione derivati da tale modello vengono quindi chiamati "Macchine di Von Neumann",
rappresentabili, in estrema sintesi dal seguente schema:

MEMORIA PROCESSORE

Figura 4.1.- La macchina di Von Neumann

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.

Se riprendiamo la definizione di Elaboratore nell’introduzione, notiamo che vi è una continuità logica


attuativa: il processore diventa quindi l’esecutore materiale, il componente che abilita la macchina a risolvere
il problema, estraendo ed eseguendo le istruzioni dalla memoria.
Nella definizione, precedente non sono nominati i “dati”, ovvero il soggetto/oggetto dell’elaborazione. Se
vogliamo descrivere quindi il processo precedente dal punto di vista dei “dati” potremmo dire che:

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

Figura 4.2.- Tipica implementazione della Macchina di Von Neumann

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:

Figura 4.3.- Tipizzazione componenti base

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.

4.1.1 La Rete di Governo

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:

Figura 4.4.- Diagramma di stato/fasi del ciclo della rete di governo

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:

- Comunicazioni con l'esterno


- Eccezioni/anomalie (memory parity error, violazione di
protezioni.)
- Allarmi nei controllo di processo (indicatori livello,
temperatura)

Il Diagramma di flusso può essere quindi modificato, inserendo un


controllo sulle FLAG di interrupt (I-FLAG), al termine
dell’esecuzione di una singola istruzione e prima di eseguirne una
nuova. In caso sia pendnete un’interruzione, allora si potrà
sospendere l’esecuzione del programma ed eseguire, in maniera
asincrona, un altro pezzo di programma il cui PC èassociato
all’evento di interrupt.
La tecnica di attuazione degli interrupt caratterizza il livello di
prestazioni consentito dalla macchina per applicazioni in TEMPO
REALE, ovvero per tutte quelle applicazioni che richiedono
deterministico il massimo tempo di risposta ad un evento esterno.

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.

4.1.2.1 Commutazione del Contesto (Context-Switch)

Si hanno le due possibilità:

a) COMMUTAZIONE DI CONTESTO INTEGRALE:


La RG si occupa di salvare tutto il pacco di registri, compresa la PSW, in modo che la routine di
servizio può utilizzarli immediatamente per eseguire le operazioni necessarie.

b) COMMUTAZIONE DI CONTESTO PARZIALE (minima):


La RG salva nello stack solo IL PC e la PSW. Se la routine di servizio ha bisogno di utilizzare altri
registri, dovrà provvedere a salvare nello stack i registri necessari e ripristinarli prima di terminare.

I due metodi hanno vantaggi e svantaggi facilmente evidenziabili:


nel caso a), si solleva il programmatore dal compito di dover eseguire le "PUSH" all'inizio della routine di
servizio le "POP" alla fine; viceversa, nel caso b), non si effettuano le PUSH e POP dei registri che non
vengono utilizzati e quindi si ha la possibilità di risparmiare tempo quando possibile.
La soluzione più efficiente (e più costosa) sta nel predisporre un certo numero di copie del pacchetto di
registri all'interno della RG stessa, in modo da poter compiere una commutazione di contesto in un singolo
ciclo macchina (processori militari).
Nell'utilizzo commerciale, si preferisce adottare la soluzione b) in modo da aver sufficienti risorse per avere
altre ottimizzazioni (ampio set di istruzioni, prefetching, etc).

4.1.2.2 Classificazione degli Interrupt

In genere distinguiamo due modalità architetturali di implementazione degli INTERRUPT:

- 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.

4.1.2.3 Interrupt multipli


In genere, quando viene eseguita una routine di servizio, le interruzioni sono disabilitate per consentire di
eseguire “atomicamente” l’intera routine di servizio, ragione questa che impone di minimizzarne il tempo.
Quando un computer dispone di molti dispositivi di I/O, si pone il problema di garantire una latenza di
risposta compatibile con le caratteristiche funzionali di ciascun dispositivo, tenuto conto che il tempo di
rilevamento (tr) potrebbe assumere valori molto alti qualora più dispositivi richiedano il servizio.
Una tecnica utilizzata per superare tali problemi consiste nel gestire gli interrupt su base prioritaria: ai
dispositivi “critici” viene assegnata una priorità alta mentre a quelli meno critici una priorità bassa.
Quando viene eseguita una routine di servizio, la priorità associata viene registrata nella PSW per cui
vengono ignorati gli interrupt provenienti da dispositivi aventi priorità inferiore, mentre vengono serviti quelli a
priorità superiore.
Ad esempio, facendo riferimento alla seguente figura:

Figura 4.5.- Priorità nel servizio alle interruzioni

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.

4.2 La Memoria dei Computer


Nel modello della Macchina di Von Neumann la memoria viene rappresentata genericamente come un unico
componente. Ragioni di opportunità economica e di maggiore efficienza, ci portano ad adottare un modello
gerarchico del tipo mostrato nella figura seguente:

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:

Figura 4.7 - Modello tassonomico della Memoria di lavoro

All’apice della classificazione si identificano due categorie principali:

1) Memorie RAM (Random Access Memory) ovvero memorie di lettura/scrittura (RWM).


2) Memorie ROM (Read Only Memory) ovvero memorie a sola lettura.

Per ognuna di queste, poi si hanno le ulteriori classificazioni, come dettagliato nel seguito:

4.3.1 RAM Statica (SRAM)


E’ una memoria costituita da flip-flop e quindi molto costose da un punto di vista realizzativo. La caratteristica
di "staticità" si riferisce al fatto che il circuito elettrico è di tipo bistabile e quindi conserverà l'informazione
binaria finché ci sarà tensione di alimentazione e finché non verrà esplicitamente cambiata da un comando.

4.3.2 RAM Dinamica


E’ una memoria di lavoro identificata, in genere, dalla sigla “DRAM” ovvero Dynamic Random Access
Memory dove il termine “dinamica” deriva dalla particolare tecnologia di base con cui è costituita ciascuna
singola cella: un condensatore ed un transistor MOS che, nel tempo, tendono a scaricarsi. Periodicamente,
prima che la tensione del condensatore scenda, da Vcc, al valore di soglia del trigger di Schmitt, si dovrà
operare un ciclo di lettura/scrittura: tale ciclo viene chiamato ciclo di “refresh” ed ha un periodo tipico
nell’ordine dei mS.
Proprio dalla necessità di provvedere periodicamente al “refresh” deriva l’attributo “dinamica” dato a questo
tipo di RAM che, rispetto alla Static RAM bassi consumi (un sono transistor/bit contro gli almeno 6 necessari
per realizzare un flip-flop di tipo D)

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.

Tra le PROM C-MOS rileviamo:

 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)

Lo schema più semplice è quello di flusso controllato da programma.

Tale schema è stato utilizzato nel paragrafo “3.2.2 Lettura


sequenza seriale asincrona Start-Stop” della parte pratica del corso,
nello stato inziale, dove, come riportato a fianco, si utilizza un ciclo di
lettura continua del bit B finché questo rimane allo stato di riposo
(=1).
L’utilizzo del controllo da programma, in questo caso, trasforma il
Computer degradandolo ad un semplice automa che non riesce a
fare altro se non aspettare il bit di START. Tale schema va quindi
usato con estrema attenzione, come verrà discusso più avanti.

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.

E' necessario distinguere due tipi di DMA:

- 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:

| R/W | Numero CANALE | Start block | End block |

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.

Possiamo classificare i Canali I/O in due tipologie/schemi:


Canale Selettore:
Il canale controlla più dispositivi ad alta velocità,
comportandosi come una CPU nei confronti dei
controllori di I/O, tutti connessi ad un unico BUS.
Il trasferimento può avvenire selezionando solo un
dispositivo alla volta, tenuto conto della presenza di
un unico BUS.

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:

 Unità funzionali (full adder, incrementatori, ..)


 Memoria (Rom, Registri MAC e MDC, ...)
 RG (Reti combinatorie e sequenziali)

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 ORIZZONTALE (a parola lunga)


è caratterizzata da molti bit per microistruzione ( > 60 bit) e quindi, generalmente, ogni singola istruzione
viene tradotta in una sola microistruzione. Ad esempio l'istruzione:
MOV R5,R3
é tradotta in una sola istruzione di microprogrammazione.

- Microprogrammazione VERTICALE (a parola corta)


Il codice operativo, identifica un indice di una tabella dove è contenuto l'indirizzo della routine associata. Le
possibili microistruzioni sono poche e la MicroRete di governo deve avere una logica molto veloce.
Nell'esempio precedente, l'istruzione verrà tradotta nelle seguenti istruzioni di microprogrammazione:
Leggi R3
Scrivi R5

- 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).

5.1 Programming model

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:

 DEFINIZIONE DELL'ORGANIZZAZIONE DELLA MEMORIA (modelli di memoria)

 DEFINIZIONE DEI TIPI DEI DATI E COME RIFERIRSI AD ESSI

 DEFINIZIONE DEL SET DI ISTRUZIONI E DELL'ORGANIZZAZIONE DEI REGISTRI

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).

5.1.1 Le Risorse della Macchina MV2


Come più volte anticipato, le Risorse disponibili a livello MV2 sono costituire dai registri, dalle locazioni di
memoria e dalle unità di I/O, unità che vengono comunque gestire tramite registri dedicati.
Nella descrizione dei registri o della memoria, il programming model dovrà poi indicare come referenziare tali
risorse ovvero, trattandosi di Macchina di Von Neumann, le diverse modalità di indirizzamento

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.

5.1.2 Linguaggio a livello MV2 - Istruzioni


In generale, possiamo identificare un'istruzione come un ordine, contenuto in una stringa di bit, ciascuno dei
quali assume un significato ben preciso.
La semantica delle istruzioni, non può essere considerata omogenea, ma è possibile individuare dei
sottoinsieme di istruzioni aventi la stessa semantica (e quindi poter individuare, al livello sottostante, in fase
di “decode” la relativa Unità Funzionale).
Genericamente, possiamo raggruppare le istruzioni nei seguenti sottoinsiemi semantici:
 Movimento dati (A=B), es.: LOAD, STORE, MOVE
 Diadiche: Aritmetiche: ADD, SUB, MUL, DIV; Logiche: AND, OR, XOR
 Monadiche: Aritmetiche: NEG; Logiche: NOT; Shift e rotazione: SHR, SHL, ROL, ROR, …
 Controllo di sequenza: Confronto e salto: JZ, JC, …; Salto incondizionato: JMP, CALL,

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.

5.1.3 Modalità di indirizzamento


Un operando ovvero l’informazione da elaborare (“dato”) può essere referenziato ovvero indirizzato secondo
una delle seguenti modalità:

5.1.3.1 OPERANDO EFFETTIVO o IMMEDIATO


In questo caso, l'informazione da elaborare è il codice stesso (numeri, maschere, I/O address) ovvero è un
valore “costante”.
In effetti, il valore è contenuto nell’istruzione stessa e quindi, se il programma è registrato nella memoria
centrale, potrebbe comunque essere modificato dal programma, trattando questa parte di codice come una
vera e propria variabile, in quanto se ne conosce la collocazione all'interno della istruzione, così come visto
nel paragrafo precedente.
Questo modo di operare, se da una parte potrebbe rendere il programma più “efficiente”, dall'altra comporta
seri problemi di instabilità del codice stesso, soprattutto in ambiente multiprogrammato, laddove viene
condiviso il codice di programma tra più utilizzatori.
Questa caratteristica è quella sfruttata dai c.d. “virus informatici” ovvero dei programmi dannosi che si
agganciano a dei programmi esistenti semplicemente cambiando delle parti del codice (es.: gli indirizzi delle
istruzioni di “salto”).
Per superare le criticità sopra richiamate e, soprattutto, per aumentare le prestazioni, è diventata una scelta
consueta l’adozione dell’architettura “Harvard”, citata in precedentemente e quindi separare l’area di
memoria contente i dai dall’area di memoria contente le istruzioni.

5.1.3.2 INDIRIZZO ESPLICITO


L’informazione da elaborare è contenuta in una locazione di memoria referenziata, in qualche modo,
dall'operando. Nella locazione di memoria possiamo trovare “subito” (o “direttamente”) l’informazione da
elaborare oppure possiamo ancora trovare, ancora una volta, solamente l’indirizzo dove potremo trovare
“indirettamente” l’informazione da elaborare. E’ evidente che la “indirezione” dell’effettiva informazione può
73
essere reiterata n. volte e per questo è opportuno utilizzare questa classificazione delle modalità
fondamentali di INDIRIZZAMENTO:

a) INDIRIZZAMENTO INDIRETTO a livello N (N = 0,1,2,)


Indicando con N il numero di accessi in memoria per determinare l'indirizzo dell'informazione da elaborare,
abbiamo le seguenti possibilità;

a0) N=0 INDIRIZZAMENTO DIRETTO o ASSOLUTO: l'informazione è contenuta nella locazione di


memoria referenziata direttamente dall'operando:

OPERANDO ---->|INFORMAZIONE|

a1) N=1 INDIRIZZAMENTO INDIRETTO: l'informazione è contenuta nella locazione di memoria


referenziata dalla locazione di memoria referenziata dall'operando.

OPERANDO ---> |INDIRIZZO| ---> |INFORMAZIONE|

a2) N=2 INDIRIZZAMENTO DIFFERITO: l'informazione è contenuta nella locazione di memoria


referenziata dalla locazione di memoria referenziata dalla locazione di memoria referenziata dalla
locazione di memoria referenziata dall'operando.

OPERANDO --> |INDIRIZZO| --> |INDIRIZZO| --> |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:

8001 -->(0001)-> |80F0| -->(00F0)-> |F703| -->(7703)->|3456|


Ind.effett. Ind.effett. Ind.eff. VALORE

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

Ad esempio, possiamo avere un “Indirizzamento modificato e relativo”: nell’istruzione viene indicato lo


scostamento (offset) ed il riferimento al registro che contiene il valore BASE. L’indirizzo effettivo sarà quindi
la somma tra la BASE e lo scostamento, che può essere, sia un valore immediato che un riferimento ad un
altro registro (registro INDICE).
74
5.1.3.3 INDIRIZZO IMPLICITO
Nelle architetture “ad accumulatore” le istruzioni diadiche prevedono un operando implicito, chiamato
“accumulatore”, destinato a contenere anche il risultato dell’operazione eseguita. In tali architetture, le
istruzioni diadiche conterranno quindi un solo operando, l’operando sorgente.

5.1.4 Progettazione del set di Istruzioni ed Ortogonalità


Una volta definita la lunghezza del campo “opcode”, è possibile progettare il set di istruzioni utilizzando uno
schema “a matrice” secondo le tre dimensioni: <opcode>, <modificatori> e <modalità di indirizzamento>
L’ortogonalità di un set di istruzioni viene definita il grado di riempimento effettivo di tale matrice ed è un utile
indicatore delle prestazioni del processore a livello MV1, con particolare riferimento alle fasi di decoding e
“search operand”. Il grado di ortogonalità d un set di istruzione, semplifica anche la programmazione diretta
“assembler” in quanto il programmatore dovrà tenere “a mente” un numero ridotto di istruzioni differenziate
da <opcode> distinti.
Ad esempio, analizzando solo le due dimensioni <opcode> e <modificatori> potremmo ottenere una tabella
del tipo:
Mod  00 01 10 11
Mnem. Opcode Normale Con Carry Decimal Flag_only
ADD 0001 x X x x
SUB 0010 x X x x
Unsigned Signed Flag_only
MUL 0011 x X x
DIV 0100 x X x

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”;

Analogamente, data una combinazione <opcode>+<modificatori> analizziamo la dimensione <modalità di


indirizzamento> potremmo ottenere una tabella più o meno piena.
Un esempio comune, anche nei processori “RISC” che, tipicamente, hanno un buon grado di ortogonalità, si
ha negli indirizzamenti modificati da registro: saranno valide solo le combinazioni che prevedono di utilizzare
solo alcuni registri “specializzati” che, per questo, vengono chiamati registri “indice”.

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).

5.1.5.1 Le Procedure o Subroutine


Una tecnica imporante che consente di strutturare i
programmi è quella di poter interrompere il flusso delle
istruzioni per eseguire un particolare segmento di codice,
specializzato ad eseguire un determianto servizio
(Procedura o Subroutine) e quindi, al termine, riprendere
l’esecuzione interrotta.Tale tecnica, in sostanza, trasforma
una sequenza comunue complicata di istruzioni, in una
nuova complessa istruzione, da usare più volte ed in
diversi punti del programma.
Come anticipato nel caso di gestione delle interruzioni, per
utilizzare tale tecnica è sufficiente disporre di un’istruzione
macchina (CALL) che, prima di effettuare il salto,
memorizzi il Progarm Counter nello stack e quindi, per
riprendere l’escuzione, disporre di un’istruzione
complementare che preleva l’indirizzo dallo stack e lo
ricarichi nel program counter (RET).

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.

5.1.5.3 Gestione dello stack - la notazione polacca inversa


La JVM utilizza lo stack per eseguire le operazioni aritmetico-logiche: in questo modo si può eliminare la
necessità di inserire gli operandi espliciti nel codice macchina.
Per tali categorie di macchine, virtuali o no, è naturale utilizzare la notazione Polacca Inversa (RPN) per
costruire espressioni aritmetico-logiche comunque complesse, basandosi sulla priorità degli operatori
artimetico-logici ed eliminando l’utilizzo delle parentesi.

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

La sequenza delle istruzioni che una JVM esegue è la seguente:


Push A;
Push B;
Push C;
IADD (esegue somma tra i due operandi al Top Of Stack e li rimpiazza con il risultato;
IMUL (esegue moltiplicazione tra i due operandi al Top Of Stack e li rimpiazza con il risultato

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.

Riprendendo il ciclo completo della rete di governo,

vediamo che vengono eseguite in sequenza le operazioni :


 Instruction Fetch
 Instruction decoding
 Read Operand (= costruzione dell’effettivo indirizzo di memoria + fetch dei dati operando)
 Execute instruction (Data Operation)
 Write Operand (=costruzione dell’effettivo indirizzo di destinazione + scrittura dati)
 Next Instruction address calculation (Aggiornamento PC)

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:

1. fetch dell'istruzione dalla memoria usando il PC come indirizzo;


2. incrementare il PC per puntare all'istruzione successiva;
3. decodificare l'istruzione;
4. leggere il valore dal registro indice (Rx);
5. calcolare la somma del campo immediato e del valore del registro indice per trovare l'indirizzo di memoria
effettivo (Rx+100).
6. leggere l'operando dalla memoria usando l’indirizzo di memoria effettivo;
7. scrivere il valore letto dalla memoria nel registro destinazione (Ry).

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.

L’attuale tecnologia permette di:

• Leggere un istruzione dalla cache-memory,


• Leggere un registro da un file-register
• Calcolare un indirizzo effettivo di memoria
• Leggere un operando una cache-memory
• Scrivere un valore in un registro

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

6.1.1 Valutazione delle prestazioni della pipeline (CPI)


Un semplice indicatore che possiamo usare per valutare le prestazioni di una pipeline e quindi del
processore “pipelined” è misurare il numero di Clock necessari mediamente per eseguire un’Istruzione (CPI)
ed il periodo di clock (inverso della frequenza di clock).
Per questo, usiamo il seguente modello:

Tm = tempo di ritardo massimo = tempo di ritardo dello stadio più lento;


k = numero di stadi della pipeline
d = ritardo di avanzamento dei valori nei latch di stadio

Il tempo di ciclo minimo (periodo di clock) è quindi determinato dalla formula:


T = Tm + d
Trascurando d, in genere << Tm , il tempo totale necessario per eseguire n istruzioni (senza salti e
partendo da pipeline vuota) è pari a:

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

Indicando con Sk il rapporto tra il tempo senza pipeline T1 ed il tempo Tk avremo:

Sk = nkT/([k + (n-1)] T) = nk/( k + (n-1))

Diagrammando la funzione in forma parametrica abbiamo:

Questo semplice modello ci mostra che:


a) L’aumento delle prestazioni massimo (= k) si “avvicina” asintoticamente quando il numero di
istruzioni senza salto condizionato è molto alto;
b) Aumentando il numero di stadi, dopo un una prima salita ripida, affievolisce l’effetto.

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.

Il seguente esempio, mostra alcuni di tali aspetti:

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)

Si possono distinguere tre classi di pipeline hazards:

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 R6Mem[R1+100]
T2 R1  R2+R3 R6Mem[R1+100
]
T3 R4  R1-R5 R1  R2+R3 R6Mem[R1+100
]
T4 R3 Mem[R2+10]* R4 R1-R5 (bloc.) R1  R2+R3 R6Mem[R1+100
]
T5 R3 Mem[R2+10]* R4 R1-R5 (bloc.) Bolla 1 R1  R2+R3 R6Mem[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]

Al clock T4 il data-hazard viene rilevato dalla fase DI cosicché le istruzioni R4  R1-R5 e R3 


Mem[R2+10] rimangono bloccate nelle fasi DI e FI (inibendo la scrittura dei latch FI/DI e del PC) e una nop
86
(Bolla 1), essere inserita nel latch DI/FO. La bolla 1 entra nella fase EI fase al clock T5.
Nei periodi T5 e T6 il data-hazard ancora esiste così le fasi FI e DI devono ancora rimanere bloccate e le
Bolla 2 e Bolla 3 vengono inserite nella fase EI.
Al T6 la fase di WO memorizza il risultato nel registro R1 che elimina data-hazard e quindi le istruzioni
precedentemente bloccate possono continuare ad attraversare la pipeline durante i periodi da T7 a T11.

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:

CPI = (numero di cicli di clock) / (numero di istruzioni)

dove:

(numero di cicli di clock) = (numero di istruzioni) + (numero di cicli bloccati)

per cui:

CPI = 1 + (numero di cicli bloccati) / (numero di istruzioni).

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:

CPI = 1 + (15%) * (3) = 1,45

Il fattore di miglioramento scende quindi a: 4,0/1,45 = 2,76

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

Disks Oltre 200 mS


Nastri e Dischi Ottici

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

Il principio di funzionamento della cache è semplice:


- Quando la CPU deve leggere un valore dalla memoria, prima controlla se tale valore è contenuto nella
cache.
- Se è contenuto, legge da cache in maniera veloce
- Se non è contenuto, legge dalla memoria principale in maniera lenta.
Risulta evidente che, più locazioni sono in cache e più veloce sarà l'elaborazione.

La buona resa della cache è assicurato dal principio di località:


 Località temporale : un programma accede molto più spesso a locazioni di memoria alle quali si è fatto
recentemente accesso piuttosto che ad altre.
 Località spaziale : Un programma accede molto più spesso a locazioni di memoria vicine tra di loro
piuttosto che lontane e sparse.

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)

La figura a fianco mostra una tipica


configurazione della cache che la vede
logicamente frapposta tra la memoria
principale e la CPU.

Per valutare correttamente l'aumento di


prestazioni dovuto alla cache, definiamo
il parametro "hit ratio" come:
h = (k-1)/k
dove k indica il numero di accessi consecutivi alla stessa locazione di memoria da parte della CPU. A parte il
primo accesso, tutti gli altri (k-1) vengono effettuati dalla cache.
Indicando con:
c tempo di accesso alla cache
m tempo di accesso alla memoria principale,
avremo che:
tempo medio di accesso = c + (1-h) m dove (1-h) = "miss ratio"

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.

Vediamo ora in dettaglio come può avvenire la gestione della cache:


91
6.2.1.1 Cache a mappatura diretta

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.

6.2.1.2 Cache n-way set-associative

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.

6.2.1.3 Sincronizzazione cache-memoria principale

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.

6.2.2 Trattamento dei salti


Uno dei problemi comuni nel pipelining e nella cache (istruzioni) è quello di mitigare gli effetti dei salti di
sequenza, siano essi condizionati che incondizionati.

Per i salti incondizionati, si può intervenire seguendo due strategie utilizzabili, sia per la gestione della
pipeline che per la memoria cache:

a) Potenziare il prefetching . Si anticipa la decodifica dei salti, informando eventualmente la cache di


procedere coerentemente,

b) Delay SLOT: eseguire comunque l’istruzione seguente un salto incondizionato, modificando, se


possibile il flusso istruzioni originario. Il compilatore deve essere ottimizzato a tale scopo e, qualora non
trovi un’istruzione idonea, inserirà comunque un NOP dopo l’istruzione di salto.
Esempio
Addr. Salto Normale Delay Slot Delay Slot ottimizzato
100 LOAD X, rA LOAD X, rA LOAD X, rA
101 ADD 1, rA ADD 1, rA JUMP 105
102 JUMP 105 JUMP 106 ADD 1, rA
103 ADD rA, rB NOP ADD rA, rB
104 SUB rC, rB ADD rA, rB SUB rC, rB
105 STORE rA, Z SUB rC, rB STORE rA, Z
106 STORE rA, Z

Per trattare i salti condizionati, prendiamo come modello il seguente segmento di codice java-like e la sua
traduzione in linguaggio assembly

if(i == 0) CPI R16,0 ; assume R16  i


k = 1; BRNE else ; salto condizionato
else then: LDI R17,1 ; assumo R17  k
k = 2; RJMP next ; salto incondizionato
else: LDI R17,2 ; assumo R17  k
next:

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

Per il salto condizionato, possiamo adottare due strategie:


a) Eseguire il fetch di entrambi i rami (valido solo per il pipelining), in analogia con la tecnica del Delay
Slot e progettare esplicitamente un rapido e non costoso “rollback” in caso di “not taken”;
b) Apettare (stall) fino a che si conosce il risultato dell’operazione “CPI R16,0 e quindi sapere se il
salto va preso (taken) oppure no (not taken)

94
c) Prevedere (Branch Prediction) se il salto verrà preso oppure no.

Per la “Branch Prediction, sono possibili ulteriori strategie.


Possiamo classificare tali strategie in due gruppi:
a) Previsioni statiche (efficienza da verificare statisticamente ex-post):
a. si prevede sempre che il salto si farà
b. si prevede sempre che il salto non si farà
c. si decide in base a diverse tipologie di codice operativo (es: salti su <= e >= salta, altrimenti
no)
b) Previsioni dinamiche
a. si prevede sulla base dell’ultima strada presa (taken/not taken)
b. si prevede sulla base di un’analisi più profonda della storia passata.

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.

Il prossimo paragrafo tratterà in dettaglio le tecniche di previsioni dinamiche

6.2.2.1 Previsione dinamica dei salti


La tecnica più elementare per gestire una previsione, è quella di usare “la prassi” ovvero temere a mente
l’ultima strada percorsa e comportarsi allo stesso modo: se la previsione risulta corretta, lasciamo tutto così,
se invece la scelta risulta errata, la correggiamo in modo da non sbagliare la prossima volta.
Una prima idea progettuale potrebbe essere quella di avere una tabella da un bit con tante entry quanti sono
gli indirizzi possibili per le istruzioni. Ad esempio, se ogni istruzione occupa 16 bit e lo spazio massimo del
programma è di 32 KBytes (processore AVR ATMEGA 328P) avremmo bisogno di una tabella da 16K righe,
ovvero 2K Bytes. In questa bitmap potremmo memorizzare “1” se il branch va preso, “0” se non va preso.
Tale soluzione è effettivamente uno spreco: moltissime righe
saranno non utilizzate, tutte quelle relative ad istruzioni non di salto.
Una Soluzione più idonea è quella di usare una “history table”
organizzata analogamente alla tabella usata per la mappatura diretta
dei blocchi di una memoria cache, così come indicato nello schema
a fianco.
Utilizziamo n bit meno significativi per accedere alla tabella
n
composta da 2 righe, ponendo in ogni entry la parte restante
dell’indiriggo (tag) e le altre informazioni necessarie (bit “Valid” e
Branch/NoBranch).
Nel caso dell’AVR ATMEGA 328P, ad esempio, possiamo pensare
di organizzare la history table con entry da 8 bit, in grado quindi di
memorizzare un tag da 6 bit. Dei 14 bit dell’indirizzo, quindi, 8 bit
bassi possono essere usati per indirizzare la tabella che sarà
composta quindi da 256 bytes soluzione molto più ragionevole ed
economica rispetto alla soluzione “immediata” da 2 KBytes.

Il semplice modello con un bit Branch/NoBranch porta ad un errore


sistemico nell’esecuzione dei cicli iterativi: la previsione viene errata
almeno due volte, una volta all’ultimo ciclo iterativo e la seconda al
rientro.
Per migliorare tale aspetto, possiamo adottare una soluzione dove si
aumenta a due il numero di bit riservati alla previsione in modo da
aumentare la distanza di Hamming (o trigger software) tra la
previsione Branch e quella NoBranch. L’algoritmo di predizione
seguirà il seguente diagramma degli stati.
95
Nel caso la previsione risulti errata, si passa ad uno stato intermedio
dove si mantiene la previsione errata e solo quando si sbaglia
ancora una volta, allora di cambia previsione come indicato nel
seguente diagramma degli stati

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

7.1 Evoluzione della scalabilità architetturale dei processori:


Le architetture basate sulla Macchina di Von Neumann si sono evolute essenzialmente secondo i due
aspetti:
 Tecnologico, concretizzatosi sostanzialmente, in un aumento della frequenza di clock.
 Funzionale, ovvero cercando di aumentare il grado di parallelismo delle operazioni interne, iniziando
dalle fasi di distribuzione e di esecuzione delle istruzioni.
Possiamo individuare i tre passi fondamentali nell’evoluzione delle funzionalità e delle prestazioni dei
processori, nella seguente tabella:

Architettura Instruction Instruction Implementazione tipica


issue Execution
Tradizionale sequenziale sequenziale processori senza pipeline
Scalare ILP sequenziale parallela processori con pipeline oppure senza pipeline
ma con unità di esecuzione multiple
Superscalare parallela parallela VLIW (Very Large Instruction Word) con unità di
ILP esecuzione multiple e pipeline.

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.

7.2 Unità Funzionali presenti nei processori superscalari


7.2.1 Decodifica parallela
Questo è il primo problema da risolvere. Il compito non è semplice e, per questo viene usato comunemente
il metodo del “predecoding” ovvero le istruzioni vengono parzialmente decodificate fin da quando sono
caricate nella instruction cache. Tale metodo viene usato dai processori PowerPC 620, PA7200, PA8000,
UltraSparc, R10000.

7.2.2 Distribuzione superscalare


L’operazione più cruciale rimane comunque quella di poter distribuire in parallelo le istruzioni alle
diverse unità di esecuzione. Infatti, all’aumentare di questo grado di parallelismo, aumenta la
possibilità di avere dipendenze di controllo e di dati tra le istruzioni rendendo vanificato il teorico
aumento di prestazioni.
Si può notare che l’incidenza della dipendenza di controllo e di dati aumenta proporzionalmente
con lo “issue rate”. Infatti, mentre nei processori scalari era possibile a livello di compilatore
inserire delle istruzioni indipendenti per riempire le “bolle” di dipendenza (vedi trattamento hazards
su pipelining) aumentando il rapporto di distribuzione occorrono molte di più istruzioni per poter
riempire gli slot non utilizzati.
Questo costituisce un effettivo ostacolo per l’effettivo aumento dello “issue rate” e per superare tale
ostacolo occorre introdurre delle politiche avanzate come il “register renaming” e lo “shelving” (lett.
“scaffalatura”).

7.2.3 Esecuzione parallela


Poter eseguire più istruzioni in parallelo è la precondizione per ottenere dei processori superscalari. Il
problema principale in questa operazione è il fatto che, una volta eseguite le istruzioni in maniera disordinata
(out of order) occorre, alla fine, ricostruire la sequenza corretta stabilita dal programma.
Nei recenti processori superscalari, per ricostruire la sequenza corretta, si disaccoppia l’esecuzione dalla
scrittura dei risultati: una volta generati i risultati dalle unità di esecuzione, lo stato del programma viene
99
aggiornato, in maniera disaccoppiata, secondo la sequenza prevista dal programma.
Quindi, se da una parte i processori superscalari tendono ad aumentare il grado di parallelismo per ottenere
prestazioni più elevate, dall’altra diventa sempre più importante poter mantenere la consistenza sequenziale.

7.2.4 Gestione delle eccezioni


Durante l’esecuzione delle istruzioni, possono accadere delle eccezioni. La ricostruzione della consistenza
sequenziale diventa qui cruciale ed aumenta l’importanza delle tecniche di conservazione di tale
consistenza.

7.3 Limitazioni architetturali al parallelismo


7.3.1 Dipendenza vera dai dati
La dipendenza vera dai dati si ha quando un’istruzione ha bisogno di un dato che deve essere calcolato da
un’altra istruzione, ad esempio:
i0: ADD r1,r2 ; esegue r1 = r1 + r2
i1: MOV r3,r1 ; esegue r4 = r1
E’ evidente che la seconda istruzione ha una dipendenza vera sui dati cioè sul registro r1 che non può
essere usato fino a quando non viene scritto dall’istruzione precedente. Tale effetto può essere così
rappresentato:

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).

7.3.2 Dipendenza da flusso procedura (salto)


La dipendenza da salto blocco l’esecuzione finché il salto non viene preso.

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)

7.3.3 Conflitto di risorse


Tale dipendenza può essere facilmente superata duplicando le risorse stesse (es.: Sommatore, Pacco
registri, memoria, cache, bus, ..), altrimenti l’effetto è analogo a quello della dipendenza sui dati:

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.

b) distribuzione (issue) ordinata e ritiro (completion) disordinato

Utilizzando lo scenario precedente e togliendo il vincolo di portare a ritiro ordinato le istruzioni,


possiamo vedere che si recupera il ciclo morto per l’attesa dell’esecuzione della I1 e quindi si
risparmia complessivamente un ciclo, portando la produzione “netta” a 6 istruzioni per 5 cicli di clock.

c) distribuzione (issue) disordinata e ritiro (completion) disordinato

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

Supponiamo di avere un processore con le seguenti caratteristiche:


 8 registri generali (R0-R7)
 istruzioni triadiche (RD=RS1 op RS2)
 le somme vengono eseguite in un ciclo, le moltiplicazioni in 2
 Parallelismo «macchina» (=ILP potenziale) pari a 2 istruzioni per ciclo di clock
 Pipeline a 3 stadi: Decode/Execute/WO

E le seguenti otto istruzioni da eseguire in sequenza:


1. MUL R3  R0, R1 ; R3=R0*R1
2. ADD R4  R0, R2 ; R4=R0+R2
3. ADD R5  R0, R1 ; R5=R0+R1
4. ADD R6  R1, R4 ; R6=R1+R4
5. MUL R7  R1, R3 ; R7=R1*R3
6. SUB R1  R0, R2 ; R1=R0-R2
7. MUL R3  R3, R1 ; R3=R3*R1
8. ADD R1  R4, R4 ; R1=R4+R4

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

Al ciclo 4 possono essere ritirate in-order le prime tre istruzioni


Al ciclo 5, R4 viene ritirato e quindi si può distribuire l’istruzione 4 e la 5
Al ciclo 6 l’struzione viene bloccata in quanto R1 è in uso e quindi non può essere sovrascritto (dip. WAR)

In sostanza, per eseguire 8 istruzioni, abbiamo impiegato 15 cicli di clock.


Introducendo la politica di distribuzione e ritiro out-of-order ed eliminando le false dipendenze sui dati
otteniamo:

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:

7.3.6 False dipendenze sui dati.


False dipendenze sui dati si verificano sia tra le istruzioni da distribuire sia tra le istruzioni
in esecuzione.
Specificatamente, prendiamo in considerazione l’eliminazione delle dipendenze di tipo
WAR (Write After Read) e WAW (Write After Write) tra registri. Considerare solo il caso di
dipendenza tra registri non è così limitativo: infatti false dipendenze su dati in moria
avvengono molto meno frequentemente, specialmente nei processori RISC.
Usando il “renaming” si possono eliminare facilmente le false dipendenze sui dati: quando
il processore incontra una dipendenza WAR o WAW, cambia nome al registro causa della
dipendenza. In questo modo i dati non sono scritti nei registri specificati ma in buffer
allocati dinamicamente.

7.3.7 Dipendenze sui controlli.


Per eliminare le dipendenze sui salti condizionali si possono seguire due strategie:
bloccare la distribuzione delle istruzioni finché non si riesce a determinare il percorso da
105
seguire oppure presumere il percorso che verrà seguito. La previsione viene fatta
stimando il risultato di ciascun salto condizionale e quindi prendendo il percorso più
probabile. In caso di errata previsione l’esecuzione verrà ripresa a partire dal salto

7.3.8 Shelving (distribuzione indiretta).

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:

Esempio di “direct issue” Esempio di uso dello “shelving” con


“reservation station”

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.

8.1 Modelli Architetturali Paralleli

I modelli architetturali di riferimento possono essere classificati in due tipologie distinte: Multiprocessori o
Multicomputer.

8.1.1 Multiprocessori o sistemi a memoria condivisa

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.

8.1.2 Multicomputer o sistemi a memoria distribuita


Tutte le CPU, intese come unità di calcolo complete di rete di governo ed unità funzionali, hanno una propria
memoria (e quindi sono delle Macchine di Von Neumann complete) comunicano tra loro tramite una rete di
interconnessione che può avere caratteristiche di “accoppiamento” più o meno lasco, comunque moplto più
lasco del modello “multiprocessore”.
Il seguente schema illustra l’architettura e come si modifica l’esempio precedente.

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.

8.1.3 Modelli ibridi


Nei paragrafi precedenti si sono messi in evidenza vantaggi e svantaggi dei due modelli di riferimento. E’
quindi immaginabile che la ricerca si sia orientata verso soluzioni che raccolgano i vantaggi di entrambi i
modelli: alta scalabilità, semplicità di realizzazione, semplicità di programmazione.
Tenuto conto che ciò che contraddistingue i due modelli è la funzionalità da assegnare all’hardware, è anche
semplice immaginare una soluzione “ibrida” che si fondi sull’astrazione a livelli di macchine virtuali,
spostando opportunamente il livello di “interazione” come da schema seguente, dove vendono rappresentati,
per semplicità, due computer cooperanti:

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 .

8.2 La struttura di interconnessione

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.

Gli aspetti progettuali di una rete riguardano:


 La topologia, ovvero dove distribuire i link e gli switch
 Il funzionamento degli switch
 Gli algoritmi di instradamento (routing) dei pacchetti

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.

8.2.1 Topologia delle reti


Per descrivere la topologia delle reti si
utilizzano dei grafi, come nella figura a fianco,
dove i link sono rappresentati dagli archi ed i
nodi individuano gli switch e le interfacce
verso i processori/computer collocato in
ciascun nodo.
Per valutare le caratteristiche delle diverse
tipologie indicate a fianco, utilizziamo i
seguenti parametri caratteristici:
 grado o fanout: indica il numero di link
connessi ad un nodo. Più alto è il grado di
un nodo e più complesso sarà l’algoritmo
di routing e più alta sarà la resilienza
potenziale ai guasti (fault tolerance)
ovvero la capacità di funzionare anche in
caso di link “fuori uso”;
 diametro: è la distanza massima tra due
nodi del grafo ovvero il numero di link che
un messaggio deve attraversare per
arrivare da un nodo all’altro. La
performance della rete dipende dal
diametro e dalla distanza media;
 bisezione dell’ampiezza di banda: è la
capacità di trasmissione complessiva che
si ottiene sezionando la rete in due parti
scollegate uguali ovvero con lo stesso
numero di nodi. Il valore si ottiene
calcolando il valore minimo, tra tute le
possibili bisezioni, dell’ampiezza di banda
totale degli archi che sono stati rimossi
per ottenere le due sezioni di rete
scollegate.
 Dimensionalità: numero di possibili scelte che è possibile fare per trasmettere un pacchetto da un
punto di partenza ad un punto di arrivo;

Analizziamo tali caratteristiche per ogni topologia di rete:


a) Stella: tutte le CPU sono connesse tramite uno switch centrale (che non viene indicato come
“nodo”). La rete ha dimensionalità zero (non c’è scelta, un solo percorso possibile) ed è di semplice
realizzazione. Ha però il grosso difetto di essere poco performante e poco resiliente: tutto il traffico
deve passare per lo switch centrale e se questo su guasta tutta la rete va fuori uso.
b) Completamente interconnessa: è l’estremo opposto della topologia a stella. Ha dimensionalità
zero (il percorso è ben definito tra due nodi dal link che li connette), massimizza la bisezione
dell’ampiezza di banda ed il diametro, è altamente resiliente in quanto il malfunzionamento di un
nodo non impedisce il funzionamento dei restanti. Di contro, Iil numero di link richiesto per k nodi è
pari a k*(k-1)/2 e quindi cresce parabolicamente con k.
c) Albero: Ha dimensionalità zero (non è possibile scegliere il percorso tra origine e destinazione), la
bisezione della larghezza di banda è pari alla capacità dei link e presenta un problema di
concentrare il traffico nei pochi nodi ai vertici. Tale difetto può essere superato tramite la tecnica del
“fat tree” ovvero raddoppiare la banda risalendo nell’albero dalle foglie fino al vertice. Nell’esempio,
se la banda alle foglie è b, nei link superiori sarà 2b ed infine nel vertice sarà 4b.
d) Anello: ha la caratteristica di avere dimensionalità pari a 1 in quanto si può scegliere , in ogni nodo,
sia il percorso di destra che quello di sinistra.

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).

8.2.2 Switching e routing

Il compito degli switch è quello di accettare i


pacchetti entranti dalle diverse porte di
ingresso ed inoltrarli alle corrette porte di
uscita. La figura a fianco illustra lo schema
di una griglia bidimensionale dove, in
ognuno dei 4 nodi, è presente uno switch a
4 porte . In tale schema è stato evidenziato
il percorso che compie un pacchetto per
essere trasmesso dalla CPU1 alla CPU2.
Non sono state disegnate le altre CPU della
rete, per comodità, ma dobbiamo sempre
immaginare che per ogni nodo vi può essere
anche una CPU “operativa”.
Come discusso nel paragrafo precedente,
questo non è l’unico percorso possibile ma è
utile per evidenziare come la trasmissione di
un pacchetto deve prevedere un tempo
finito e quindi fotografando la rete ad un
certo istante troveremo le diverse
informazioni del pacchetto “spalmate” lungo
la rete ovvero nei link e negli switch.
La parte iniziale del pacchetto è già arrivata allo switch D e quindi alla CPU2, la parre centrale è nello switch
C mentre la parte finale è ancora nello switch A e nella CPU1.
In questa figura si è anche illustrata una delle strategie possibili di switching: il “circuit switching” che
consiste nel “prenotare”, prima di avviare la trasmissione, tutte le risorse (link, porte e buffer) necessarie per
arrivare da CPU1 a CPU2. Tale strategia, pur se garantisce la trasmissione alla massima velocità, non è
ottimale in quanto richiede tempo per la prenotazione con il rischio che, una volta prenotate le risorse, il
circuito potrebbe essere sottoutilizzato perché, nel frattempo, la CPU1 sta facendo altre attività prioritarie e
quindi non è pronta ad iniziare a spedire il pacchetto, costringendo altre trasmissioni a rimanere in attesa che
si liberino le relative risorse. Tale modalità può essere paragonata a quella usata nella rete telefonica
tradizionale punto-punto: quando un telefono chiamante seleziona il numero del chiamato, la rete telefonica
prenota il circuito di collegamento tra i due ed il dialogo potrà avvenire alla massima velocità: la rete nel suo
complesso però è poco sfruttata in quanto le risorse rimangono riservate anche se si sta “in silenzio” e, per
questo la tariffazione avviene in base al tempo di collegamento e non al traffico.
Un secondo tipo di strategia è basata sul “packet switching” di tipo “store and forward” dove l’intero
pacchetto viene trasmesso al primo switch che gestisce l’instradamento successivo, sempre per pacchetti
interi che quindi si troveranno allocati, in tempi successivi, negli switch intermedi fino a destinazione:

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.

Le tecniche e strategie usate nella pratica sono


molto più complesse e sono correlate ad una
strategia di più alto livello ovvero agli algoritmi di
“routing” dei pacchetti, necessaria per gestire al
meglio le reti n-dimensionali.
L’obiettivo principale di tali algoritmi è quello di
suddividere uniformemente il traffico sui diversi link
disponibili evitando le situazioni di deadlock, ovvero
situazioni dove nessun pacchetto può procedere
perché una delle risorse necessarie è già stata
prenotata e chi l’ha prenotata ha ora bisogno della
risorsa prenotata da altri, in uno schema “circolare”
come illustrato nella figura a fianco, dove ogni CPU
vorrebbe inviare un pacchetto alla CPU
diametralmente opposta e sta cercando di prenotare
il relativo circuito, tutte nello stesso istante di tempo!.
La risoluzione di tale problematica esula dagli scopi del corso e pertanto la si lascia semplicemente come
“problema da tenere presente” e si illustrano in sintesi le tipologie di algoritmo basilari:
a) Source routing: Lo switch sorgente determina l’intero percorso da seguire, conoscendo la
tipopologia di rete, e lo scrive nella busta;
b) Routig distrubuito: ogni switch è in grado di individuare la porta migliore verso cui instradare il
pacchetto, o in forma statica o adattivamente, in base al traffico;
c) Dimensional routing: è un modo per evitare i rischi del deadlock: la direzione viene scelta
privilegiando il mantenimento dell’asse iniziale fino al valore massimo possibile in quella direzione.

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;

b) Packet switching (Store and forward): Tp = Ta + n * (p/b + Td) + p/b


Dove:
Tp è il tempo di latenza totale per trasmettere un pacchetto comporto da p bit in una direzione (half
duplex);
Ta è il tempo di “setup” necessario costruire il pacchetto dati comprensivo di busta;
n è il numero di switch da attraversare,
Td è il tempo di ritardo introdotto da ciascuno switch per l’elaborazione del pacchetto e per la
gestione delle code – per semplifcità consideriamo un tempo medio;
p è la dimensione in bit del pacchetto complessivo da trasmettere, busta compresa
b è l’ampiezza di banda del circuito;
Nota: L’ultimo temine (p/b) di tale formula indica il tempo impidgato dallo switch finale per
trasmettere i dati alla CPU di destinazione.

c) Algoritmi ottimizzati: Tp = Ta + p/b


Indichiamo solo il caso migliore ovvero il limite non superabile, qualunque sia la strategia di
switching adottata:

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

Le prestazioni hardware sono


solo un componente che
influisce sulle prestazioni
complessive delle architetture
parallele. Le prestazioni da
misurare devono essere riferite
alla percezione che ha l’utente
complessivamente ai livelli più
alti, avendo come riferimento il
modello ibrido introdotto nel
paragrafo 9.5.1.3.
La seguente figura mostra come
l’aumento delle prestazioni
percepite dall’utilizzatore
(speedup) dipenda fortemente
dalla tipologia di problema di
calcolo da risolvere, come
indicato nel grafico degli esiti
rilevati per diverse tipologie di
problemi.
La retta ideale non vene mai raggiunta, nemmeno nei problemi facilmente scomponibili in sottoprocessi
paralleli (problema classico in fisica degli N corpi). Addirittura nei problemi di inversione matrici, non si riesce
mai a superare lo speedup di 5, anche aumentando il numero di CPU (il peggioramento è dovuto
all’aumento della latenza HW).
Tale comportamento discende principalmente dalla legge di Amdahl.

8.4.1 Legge di Amdahl

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.

Analizziamo ora i dettagli progettuali di ciascuna delle classi sopra esposte.

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

Il primo computer messo in


commercio (ILLIAC IV) risale
al 1968 e si basa su una
matrice di elementi di calcolo
di tipo P-M, connessi in
topologia a “griglia”
L’unità di controllo trasmette
l’istruzione, anche
complessa, da eseguire a
tutti gli elementi di calcolo,
definendo per ciascuno di
essi l’ambito di memoria di
competenza.
Nelle successive
implementazioni di Array
processor (Thinkng Machine
CM-2 e Maspar M-2), nel
tentativo di aumentarne la
commercializzazione, si sono adottate scelte progettuali differenti come quella di ridurre l’elemento di calcolo
a delle semplici “ALU” da 1 bit (CM2)
Altre celte riguardano lil grado d autonomia locale dei singoli elementi di calcolo che potrebbe scegliere se
eseguire o meno l’istruzione trasmessa sulla base dei dati locali trattati.
Una scelta comune è quella di adottare una topologia a griglia rettangolare per la sua caratteristica di
scalabilità.

2) Vector Processor

Anche qui le prime implementazioni sono state eseguire da


Seymour Cray a partire dal 1976. Tali architetture erano
specializzate ad effettuare calcoli intensivi del tipo:
for(i=0;i<n;i++)a[i]=b[i]+c[i] dove è evidente la possibilità di
un alto grado di parallelizzazione.
L’ALU vettoriale dovrà eseguire, in genere delle operazioni
miste vettoriali/scalari del tipo elencato nella tabella
seguente:

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 relativa al processore: vi sono solo i seguenti vincoli:


1. ogni CPU vede le scritture fatte da una CPU nello stesso ordine con cui sono effettuate;
2. tutte le CPU vedono le scritture per una locazione, nello stesso identico ordine;
in sostanza, viene tolto il vincolo globale visto in precedenza, (ora diventa valido il risultato barrato) e lo
si lascia solo relativo alle sequenze di ciascuna CPU

 Modelli a consistenza debole: viene rimosso il vincolo n. 1;

 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.

Di seguto un’implementazione di tale schema (Rete Omega)

8.5.2.4 NUMA – Non Uniform Memory Access

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:

Gli elementi che caratterizzano i sistemi NUMA sono:


1. Esiste un unico spazio di indirizzamento per tutte le CPU;
2. Accesso alla memoria remota esclusivamente tramite istruzioni “controllabili”, di tipo LOAD/STORE;
3. Accesso alla memoria locale molto più veloce (almeno un fattore 10) rispetto a quelli per la memoria
remota.

Inoltre, possiamo distinguere l’utilizzo della cache memory:


a) NC-NUMA, quando l’accesso alla memoria remota non è mascherato ovvero non viene intermediato
da una cache. Per tali sistemi è fondamentale allocare pagine di memoria nella CPU che la utilizza
più frequentemente (località temporale – page scanner);
b) CC-NUMA, quando per accedere alla memoria remota si utilizza un sistema di cache coerente e
quindi consente di superare agevolmente le inefficienze nelle distribuzione delle pagine di memoria
tra le diverse CPU. L’implementazione è più complessa in quanto ciascuna CPU deve avere
conoscenza della directory delle pagine, come da schema seguente:
125
8.5.2.5 COMA – Cache Only Memory Access

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

Passare da un’architettura Multiprocessore NUMA ad un’architettura Multicomputer, il passo è breve: oltre


alla memoria locale, aggiungiamo anche le unità di I/O ed otteniamo uno schema del tipo seguente:

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

8.5.3.1 MPP – Massive Parallel Processor

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.

Entrambe le tipologie basano il loro


funzionamento sullo scheduler dei job che
l’utente sottopone al sistema. In base al
numero di CPU richieste da ciascun job, lo
scheduler definisce l’ordine di esecuzione
secondo tre modalità;
a) FIFO: dopo che un job è partito, si controlla
se vi sono CPU sufficienti per farne eseguire
un altro, altrimenti si attende (e si blocca la
coda)
b) FIFO on seconda scelta: invece di bloccare
l’intera coda, si scartano i job non adatti si
cercano nella coda quelli adatti
c) FIFO e slot temporale: Ciascun job dichiara le CPU necessarie e per quanto tempo e quindi si può
riempire al massimo il rettangolo CPU/tempo.

128

Potrebbero piacerti anche