Sei sulla pagina 1di 61

SISTEMI DI

ELABORAZIONE
Angela Giugliano

LAUREA MAGISTRALE IN ING. INFORMATICA


UNIVERSITÀ DEGLI STUDI DI SALERNO
A.A. 2015/2016
PROGETTAZIONE ARCHITETTURALE DI UN SISTEMA DI ELABORAZIONE 1

CONCETTI PRELIMINARI 1
MACCHINE ASINCRONE E SINCRONE 1
MACCHINE COMBINATORIE E SEQUENZIALI 1
APPROCCI RISC E CISC 1
MACCHINA DI TURING 1
MODELLO DI VON NEUMANN 1
INTRODUZIONE 3
MODELLO DI RIFERIMENTO 3
PRESTAZIONI DI UN SISTEMA DI ELABORAZIONE 3
IL PROCESSORE 5
ISTRUZIONI DI UN PROCESSORE 5
MEMORIA E INDIRIZZI 6
PROCESSORE MONOCICLO 7
PROCESSORE MULTICICLO 10
PIPELINING 13
INTERRUZIONI ED ECCEZIONI 16
INTERRUZIONI 16
ECCEZIONI 18
GESTIONE DELLE INTERRUZIONI 19
LA GERARCHIA DI MEMORIA 25
PROBLEMATICHE PRINCIPALI 25
LIVELLI DI MEMORIA 26
SOLUZIONI ARCHITETTURALI E CONCLUSIONI 30
L’ESECUZIONE FUORI ORDINE 33
PIPELINE CON UNITÀ FUNZIONALI MULTICICLO 33
METODO DEL COMPLETAMENTO IN ORDINE DELLE ISTRUZIONI 36
METODO DEL BUFFER DI RIORDINAMENTO 36
METODO DELL’HISTORY BUFFER 38
GESTIONE DEI CONFLITTI DI CONTROLLO 40

ANALISI PRESTAZIONALE DI UN SISTEMA DI ELABORAZIONE 42

INTRODUZIONE 43
INDICI DELLA QUALITY OF SERVICE 43
IL CICLO DI VITA DI UN SISTEMA IT 47
MODELLAZIONE DI UN SISTEMA 49
TIPI DI MODELLAZIONE 49
QUEUEING NETWORK (QN) 50
QUEUEING DISCIPLINES 56
PARAMETRI PRESTAZIONALI 57
PROGETTAZIONE ARCHITETTURALE
DI UN SISTEMA DI ELABORAZIONE
Concetti Preliminari
Macchine asincrone e sincrone
Una macchina asincrona è un dispositivo che non presenta alcuna temporizzazione.
Una macchina sincrona invece è caratterizzata da un segnale che scandisce il tempo (clock).

Macchine combinatorie e sequenziali


Una macchina combinatoria è un dispositivo modellabile con una funzione:

Una macchina sequenziale invece è un dispositivo modellabile con una funzione:

Dove y è l’uscita della macchina, x è il suo stato presente, u è il suo ingresso.

Approcci RISC e CISC


L'acronimo RISC (Reduced Instruction Set Computing) indica una filosofia di progettazione di
architetture per microprocessori che predilige la linearità e la semplicità. Questa semplicità di
progettazione permette di realizzare microprocessori con un set ridotto e semplificato di istruzioni
elementari e poco espressive.
L’acronimo CISC (Complex Instruction Set Computing) indica invece un'architettura
per microprocessori caratterizzata da un set di istruzioni complesse, elaborate ed espressive.

Macchina di Turing
Una macchina di Turing è una macchina ideale che manipola i dati contenuti su un nastro di lunghezza
potenzialmente infinita, secondo un insieme prefissato di regole. In pratica, essa è un modello astratto in
grado di eseguire algoritmi attraverso la lettura e la scrittura di simboli (appartenenti ad un alfabeto finito)
su un nastro infinito. La macchina si evolve nel tempo e ad ogni istante si può trovare in uno stato interno
ben determinato, facente parte di un insieme finito di stati.

Modello di Von Neumann


Il modello di Von Neumann è l'architettura hardware su cui si basa la maggior parte dei moderni computer
programmabili. Esso è caratterizzato dal condividere i dati del programma e le istruzioni del programma
nello stesso spazio di memoria. Per tale caratteristica, si contrappone all'architettura Harvard, nella quale
invece i dati del programma e le istruzioni del programma sono memorizzati in spazi di memoria distinti.

1
Lo schema del modello di Von Neumann si basa su cinque componenti fondamentali:
CPU o processore (unità centrale di elaborazione) che si divide a sua volta in:
o Unità operativa o datapath, nella quale uno dei sottosistemi più rilevanti è l'ALU (unità
aritmetica e logica, nella quale è presente un registro detto accumulatore, che fa da ponte
tra input e output)
o Unità di controllo o control unit
Unità di memoria, intesa come memoria di lavoro o memoria principale (RAM, Random Access
Memory)
Unità di input, tramite la quale i dati vengono inseriti nel calcolatore per essere elaborati
Unità di output, necessaria affinché i dati elaborati possano essere restituiti all'operatore
Bus, un canale che collega tutti i componenti fra loro

Figura 1: Schema del modello di Von Neumann

Di fondamentale importanza è il ciclo del processore, detto anche ciclo di fetch-decode-execute, il quale si
riferisce alla dinamica generale del funzionamento del processore stesso. In pratica, infatti, un processore
esegue iterativamente tre operazioni: preleva (fetch) un'istruzione dalla memoria principale, in seguito
decodifica (decode) ovvero interpreta l'istruzione, infine la esegue (execute), combinandola coi dati relativi
all'istruzione stessa.

2
Introduzione
Modello di riferimento
Un sistema di elaborazione, detto anche computer o calcolatore elettronico, è un sistema in grado di
risolvere automaticamente problemi e trattare in maniera automatica informazioni, eseguendo istruzioni.

I sistemi di elaborazione si basano sul modello di Von Neumann, il quale è una implementazione della
macchina di Turing: la memoria del modello di Von Neumann corrisponde al nastro potenzialmente infinito
della macchina di Turing. La memoria, in particolare, rappresenta lo stato della computazione ed i valori di
ingresso e di uscita del modello di Von Neumann.
Ciò vuol dire che il passaggio da uno stato ad un altro della computazione rappresenta il trascorrere del
tempo. Si sottolinea che nei sistemi di elaborazione il tempo è rappresentato come una variabile discreta
ma, come sappiamo, nella realtà esso è una variabile continua.

A tale proposito, possiamo dire che le macchine asincrone sono gli unici dispositivi fisicamente realizzabili
nei quali il tempo è visto come una variabile continua. Il valore computato da esse, però, è significativo
soltanto nei loro stati stabili, dunque tale caratteristica discretizza in un certo modo il tempo.
Al contrario, le macchine sincrone consentono il passaggio da un asse dei tempi continuo ad uno discreto,
grazie alla presenta di un segnale di clock, che è appunto una variabile discreta.

Il modello di Von Neumann, in particolare, sostituisce alla macchina di Turing, che è una macchina cablata,
una macchina generale in grado di cambiare dinamicamente la propria struttura in modo da adattarsi alle
istruzioni da eseguire. Una macchina di Turing, infatti, è caratterizzata da N automi, uno per ogni possibile
istruzione eseguibile. L’aspetto fondante del modello di Von Neumann, invece, è il ciclo del processore, che
sostituisce agli N automi della macchina di Turing un meta-automa, in grado di descrivere il modo in cui si
può computare tutto ciò che è computabile.

Prestazioni di un sistema di elaborazione


In un sistema di elaborazioni, gioca un ruolo fondamentale il tempo di esecuzione T, generalmente definito,
in accordo al modello di Von Neumann, come:

Dove la sommatoria è stesa su tutte le classi di istruzioni, inoltre:


è il numero di istruzioni di una i-esima classe
è il tempo necessario ad eseguire l’istruzione della i-esima classe
è la frequenza di clock

In particolare, la tecnologia elettronica è interessata a diminuire il tempo di elaborazione, aumentando la


frequenza di clock. Per lo stesso motivo, l’obiettivo delle architetture hardware è diminuire il numeratore
nella formula di T. Il tempo di elaborazione, tuttavia, non è l’unico parametro determinante all’interno di
un sistema di elaborazione.

3
Difatti, le tre variabili fondamentali per le prestazioni di un sistema di elaborazione sono:
Tempo
Memoria
Costo
Esse sono inversamente proporzionali fra di loro e devono essere tenute in conto nella progettazione di un
sistema di elaborazione.

A tale proposito, possiamo citare la differenza tra il processore monociclo ed il processore multiciclo.
Il primo è una macchina combinatoria caratterizzata da un tempo unico per l’esecuzione di ogni tipo di
istruzione. Al contrario, il secondo è una macchina sequenziale che specializza il tempo di esecuzione di
un’istruzione in base al tipo di istruzione: ciò comporta un miglioramento delle prestazioni globali rispetto
al processore monociclo, ma un peggioramento delle prestazioni locali.

Questo esempio ci fa comprendere che, in un sistema di elaborazione, il miglioramento di una variabile


solitamente corrisponde al peggioramento di un’altra.

4
Il Processore
Istruzioni di un processore
In un processore, una generica istruzione ha tre componenti:
Un codice operativo che specifica l’operazione da effettuare
Almeno un operando, il quale è il valore sul quale vogliamo effettuare l’operazione specificata dal
codice operativo
Un’informazione su dove si deve salvare il risultato dell’operazione perché, in accordo al modello, il
risultato di ogni computazione deve essere salvato in memoria

OPERATIVE CODE OPERANDS DESTINATION

Quando si progetta un processore bisogna fare delle scelte. Di solito tali scelte influenzano il formato delle
istruzioni, il set delle istruzioni e l’architettura del processore (datapath e control unit). Infatti, quando si
decide il formato di un’istruzione, si impone una struttura sintattica alla stringa di bit che costituirà
l’istruzione. Dunque, per rispettare tale sintassi, bisognerà decidere differenti percorsi attraverso il
processore, corrispondenti alle operazioni indicate nel codice operativo delle istruzioni, in modo da
produrre un certo effetto. Perciò, per differenziare le funzioni bisognerà differenziare il percorso dalla
sorgente alla destinazione del processore.

Nel MIPS, ad esempio, le istruzioni sono lunghe 32 bit, di cui 6 bit sono dedicati al codice operativo.
Inoltre, l’architettura del MIPS assume che gli operandi delle istruzioni siano dei registri.
Un registro è una piccola porzione di memoria che non risiede nella memoria principale, bensì nella CPU,
per aumentare le performance del processore stesso: infatti, senza altre assunzioni tecnologiche, il tempo
di andata e ritorno dalla CPU alla memoria è maggiore del tempo di andata e ritorno dalla CPU ai registri.
Tale affermazione ha due motivazioni:
Una motivazione logica: CPU e memoria sono macchine sequenziali quindi bisogna sincronizzarle in
accordo alla velocità della macchina più lenta fra le due
Una motivazione tecnologica: CPU e memoria si trovano in due differenti porzioni di silicio, quindi
occorre più tempo per andare da una porzione ad un’altra
Quindi, nel MIPS tutte le istruzioni devono impiegare operandi che utilizzano i registri della CPU.
I registri della CPU sono contenuti in un array definito register file. Va detto che più grande scegliamo il
register file e più bit serviranno per codificare l’indirizzo di ogni singolo registro. Nel MIPS, ad esempio, il
register file contiene 32 registri, perciò abbiamo bisogno di 5 bit per indirizzarli.

Per quanto concerne le istruzioni, possiamo distinguere 3 tipi di istruzioni:


Istruzioni di accesso alla memoria
Istruzioni di salto
Tutte le altre istruzioni
Questo vuol dire che il datapath deve permettere due flussi differenti di informazione: uno che attraversa
l’architettura del processore senza passare per la memoria ed un altro che accede alla memoria.
Inoltre, le istruzioni di salto devono essere presenti per far sì che il processore possa computare tutto ciò
che è computabile dalla macchina di Turing. Tali istruzioni, per definizione, contengono in un operando
l’indirizzo dell’istruzione a cui saltare.

5
Memoria e indirizzi
Ricordiamo che la macchina di Turing è dotata di un nastro infinito, mentre quella di Von Neumann
presenta un nastro finito, ovvero la memoria. Nello specifico, maggiore è la memoria, più complessa è la
computazione che possiamo effettuare su tale macchina. Perciò, vorremmo una memoria grande. Ma avere
una memoria grande richiede stringhe più lunghe di bit per specificarne gli indirizzi, in quanto sussiste una
relazione logaritmica tra il numero di bit che dobbiamo specificare per accedere alla memoria ed il numero
di elementi che abbiamo in memoria. Dobbiamo trovare un compromesso. Il MIPS, ad esempio, assume
che la memoria sia un array unidimensionale.
In tal caso, il modo più semplice per indirizzarne gli elementi è suddividere gli indirizzi in due porzioni:
Indirizzo base, uguale per tutti gli elementi dell’array
Offset, che indica i vari elementi dell’array
Questo vuol dire che si può usare un singolo registro per contenere l’indirizzo base, dunque specificando
l’offset si può accedere alla porzione di memoria di interesse.
Pertanto, le istruzioni relative all’accesso in memoria, oltre al codice operativo, presentano:
Un operando, che è il registro in cui bisogna salvare i dati provenienti dalla memoria (nel caso di
una load) o da cui bisogna prelevare i dati per salvarli in memoria (nel caso di una store)
Un altro operando, che è il registro contenente l’indirizzo base per accedere alla memoria
Un offset.
Ora, abbiamo detto che una istruzione è costituita da 32 bit, di cui 6 dedicati al codice operativo.
In particolare, nelle istruzioni di accesso alla memoria, dobbiamo specificare due registri, ciascuno da 5 bit
(in accordo alla dimensione del register file del MIPS): ciò vuol dire che restano 16 bit per indicare l’offset.
Dunque, la più grande porzione di memoria indirizzabile è 216k ovvero 64k.

6
Processore monociclo
Il datapath del processore monociclo deriva immediatamente dall’implementazione del modello di Von
Neumann. Vediamo come esso possa essere mappato sul ciclo del processore.

Figura 2: Datapath del processore monociclo

Innanzitutto, sappiamo che il processore effettua iterativamente il ciclo fetch-decode-execute.


In accordo al modello, i dati di input del processore risiedono nella memoria, perciò dobbiamo estrarre
(fetch) i dati dalla memoria, inserirli nei registri e poi computare le istruzioni. Infine, ancora come vincolo
del modello, l’output del processore, ovvero il risultato della computazione, deve essere salvato in
memoria. Il modello, quindi, impone che la prima fase sia la fase di fetch, ovvero di prelevamento
dell’istruzione dalla memoria. Sappiamo infatti che, sempre in accordo al modello, sia le istruzioni sia i dati
sono in memoria. In particolare, per accedere ad una porzione specifica di memoria, bisogna ottenere
l’indirizzo di tale porzione di memoria.

Una volta che l’istruzione viene prelevata dalla memoria, il processore deve essere pronto ad eseguire la
prossima istruzione. Quindi, la lunghezza dell’istruzione influenza anche il modo in cui si va ad aggiornare il
program counter (PC), che contiene l’indirizzo della prossima istruzione da eseguire. Ciò vuol dire che la
lunghezza dell’istruzione deve essere un multiplo della singola unità di memoria indirizzata. La memoria che
andiamo a considerare è byte-addressable, quindi l’istruzione deve avere una lunghezza che sia multiplo di
8. Ecco perché le istruzioni sono lunghe 32 bit.

Perciò, quando dobbiamo aggiornare il program counter con la prossima istruzione, si addizionano 4 byte
alla istruzione presente, per passare alla istruzione successiva. A tale scopo è presente il primo
addizionatore nella figura.

Abbiamo anche visto che nelle istruzioni di accesso alla memoria è presente un offset, che va addizionato
all’indirizzo base per generare l’indirizzo di memoria desiderato. A tale scopo, in figura è presente un
collegamento diretto tra l’uscita della memoria istruzioni e l’ALU, il quale oltrepassa il register file. L’offset
della istruzione va in input all’ALU insieme all’output del register file, che è l’indirizzo base.

7
Sommando i due, avremo l’indirizzo di memoria cercato e potremo accedere alla porzione di memoria
corrispondente a tale indirizzo. Se l’istruzione è una load, il dato presente in tale porzione verrà caricato nel
register file. Se invece l’istruzione è una store, in quella porzione di memoria verrà salvato il dato che
proviene dal register file. A tale scopo, in figura vediamo un’altra connessione che prende l’output del
register file e lo pone in ingresso alla porta dati della memoria dati. Questa architettura discende
direttamente dall’implementare il ciclo del processore con uno tra i due approcci RISC e CISC.

Ora, passiamo alle istruzioni di salto. Esse rappresentano una classe di istruzioni in cui l’operando di
destinazione è il program counter. A tale scopo, nella figura è presente un altro addizionatore, che prende
in ingresso l’output del primo addizionatore (ovvero il valore del program counter + 4) e vi aggiunge
qualcosa che è all’interno dell’istruzione. Quindi, una parte dell’istruzione di salto è interpretata come un
offset rispetto al valore corrente del program counter. Per questo motivo, vorremmo avere più bit possibili
per specificare tale offset, in modo da effettuare salti più lontani.

Figura 3: Processore monociclo

Ricordiamo però che i programmi soddisfano il principio di località a causa di vincoli di progettazione,
quindi in realtà non abbiamo bisogno di moltissimi bit per specificare salti molto lunghi. Pensiamo alla
programmazione strutturata o alla programmazione ad oggetti: una struttura dati o un oggetto
rappresentano un modo per imporre una struttura ai dati e per trattarli come un unico pezzo di
informazione. Così, si può accedere a porzioni sempre più grandi di memoria ma tale memoria è costituita
da blocchi di indirizzi che vengono acceduti localmente, ed una volta che si esce da uno di quei blocchi, vi si
ritornerà con una probabilità molto bassa.

8
Le conseguenze di ciò sono positive sia per il processore sia per la memoria perché ci consentono di
implementare una memoria grande pur avendo una memoria fisica relativamente piccola, dunque siccome
la memoria fisica è piccola, può essere resa più veloce.

Abbiamo quindi bisogno di un modo per selezionare quale indirizzo fornire al PC e perciò nel processore è
presente un multiplexer che restituisce in output il prossimo contenuto del PC, selezionando tra il corrente
valore del PC + 4 oppure l’output dell’ultima computazione. Si evince quindi l’importanza di una control
unit, che riceva il codice operativo, riconosca l’istruzione di salto e valuti se la condizione di salto è
verificata o meno. La verifica della condizione di salto è effettuata considerando l’output dell’ALU, la quale
compara gli operandi dell’istruzione e dà in output la relazione logica fra i due. Perciò nella istruzione di
salto abbiamo bisogno di specificare gli operandi da confrontare per capire se il salto va effettuato o meno:
in tale istruzione, ovviamente composta da 32 bit, abbiamo 6 bit per il codice operativo, 5 bit per il primo
operando, 5 bit per il secondo, quindi i restanti 16 bit costituiscono l’offset. Perciò, si può saltare di 216
ovvero 64 posizioni dall’indirizzo corrente del program counter. Siccome potremmo voler saltare in avanti o
indietro rispetto all’indirizzo corrente, questo intervallo è diviso in 32 indirizzi precedenti e 32 successivi.
Come abbiamo detto, la control unit selezionerà, nel caso di salto, il percorso da seguire per ottenere il
risultato specificato nel codice operativo mentre il multiplexer ci dirà quale sarà il contenuto del PC.

Al register file, inoltre, la control unit invia un segnale che specifica se leggere o scrivere da esso. Siccome a
volte si vuol dare in input all’ALU un operando di un’istruzione proveniente dalla memoria istruzioni,
mentre altre volte le si vuol dare in input il contenuto di un registro, nel processore è presente un
multiplexer a monte dell’ALU, abilitato dalla control unit in accordo al codice operativo, che seleziona uno
fra tali due casi. L’altro input dell’ALU non presenta un multiplexer perché in ogni istruzione si ha almeno
un operando che è in un registro, quindi è certo che quell’input sarà l’output del register file.

Dunque, per fare una metafora, si può dire che la control unit è come un capostazione, il processore è una
ferrovia costituita da molti binari, mentre i suoi componenti sono delle stazioni. Ogni stazione è
caratterizzata da alcuni addetti ai lavori che effettuano specifiche manutenzioni sui treni. I treni sono
essenzialmente sequenze di bit, che possono seguire differenti percorsi da una stazione all’altra in base al
tipo di treno, grazie ad uno switching effettuato dal capostazione.

Ma nel processore monociclo non vediamo alcun clock, esso è una macchina combinatoria, quindi ci
domandiamo come sia possibile che tale macchina compia azioni che richiedono una tempificazione.
In realtà, nel processore monociclo il tempo si nasconde, nel senso che l’unico dispositivo che osserva lo
scorrere del tempo è il PC, il quale è una memoria, che è perciò controllata da un clock. Inoltre, flusso di
informazione è in pratica un flusso di elettroni che attraversa i componenti del processore: tale fluire nello
spazio richiede fisicamente del tempo. Quindi, il ciclo che abbiamo descritto in termini di tempo è qui
implementato in termini di spazio.

Si possono perciò eseguire istruzioni in modo sequenziale: si può computare un’istruzione dopo aver
aspettato il tempo della computazione della precedente istruzione, per cambiare il valore del PC. Questo è
però un problema per una macchina combinatoria. Per risolverlo, si osserva che, a seconda delle istruzioni,
abbiamo differenti percorsi che richiedono tempi diversi per andare dalla sorgente alla destinazione:
dunque, per aggiornare il PC in modo sempre coerente, dobbiamo dimensionare il suo tempo di attesa sul
tempo dell’istruzione che richiede il tempo maggiore. Questo, però, comportano uno spreco di tempo
quando si compiono istruzioni che richiedono tempi di esecuzione minori di quello prefissato per il PC.

9
Possiamo migliorare le prestazioni di tale macchina cambiando approccio, per renderla più veloce.
Dobbiamo far sì che essa impieghi tempi differenti per istruzioni differenti; per fare ciò, dobbiamo forzare
ogni istruzione ad andare dalla stessa sorgente alla stessa destinazione passando per tutti i dispositivi,
rendendo così tutti i percorsi uguali. Il numero di istruzioni non cambia, ma cambia il tempo per eseguirle.

Ritornando alla metafora della ferrovia, ogni stazione verrà attraversata da ogni treno, anche se alcuni treni
non devono subire nessuna manutenzione in certe stazioni. In questo modo non c’è switching, il
capostazione non ha memoria. Ogni nuovo treno è caratterizzato da una etichetta; ogni volta che un treno
esce da una stazione, un nuovo treno può partire. Quindi, nel presente caso, il tempo per transitare dalla
prima all’ultima stazione sarà più lungo rispetto al caso precedente. Il miglioramento sta nel fatto che i
treni fluiranno uno dietro l’altro a distanza di una stazione. Quindi un singolo treno impiega più tempo ad
attraversare tutto il percorso, ma una sequenza di treni impiegherà meno tempo per essere processata.
Questo approccio è definito pipelining e sarà trattato nel seguito.

Processore multiciclo
Notiamo che, nel processore, quando le informazioni iniziano a fluire per la prima volta, solo i dispositivi
iniziali sono interessati da tale fluire. Quindi, tali dispositivi devono implementare la fetch. Dunque, il flusso
di informazione raggiunge la control unit e solo allora essa saprà quale istruzione bisogna compiere. Ciò
implica che nella fase di fetch la control unit non ha conoscenza dell’istruzione da computare, perciò tale
fase deve essere uguale per tutte le istruzioni. Successivamente si effettua la decode, ovvero si prendono
gli operandi dai registri (del register file) i cui indirizzi sono specificati nell’istruzione stessa. Dopo un certo
tempo, la decode produce in output una informazione che dirà all’ALU cosa fare sugli operandi. Infine, si
esegue la execute: il flusso informativo si muoverà e andrà all’ALU per eseguire la computazione dettata
dall’istruzione. Dopo l’esecuzione dell’istruzione, si ha un output, che può essere il valore degli operandi o
un indirizzo di memoria o l’indirizzo che deve essere salvato nel PC in caso di salto. Quindi, dobbiamo
scrivere l’output, prima di fare ciò dobbiamo salvare in memoria nel caso di load.

In particolare, in caso di load bisogna effettuare varie operazioni, in caso di store altre operazioni, e così per
i salti e per tutte le altre possibili istruzioni: ogni istruzione è costituita da operazioni che potrebbero essere
inutili per un altro tipo di istruzione. Si potrebbe quindi misurare il tempo che impiega ogni operazione, e si
potrebbe rendere la control unit una macchina sequenziale in grado di controllare differenti sequenze di
operazioni. Solo quando essa vede che l’istruzione è stata eseguita, può aggiornare il PC.
Questo è l’approccio del processore multiciclo.

Sicuramente il flusso informativo, in quanto flusso fisico, continua a procedere, ma siccome la control unit è
progettata in un certo modo, ciò non produrrà alcun effetto, a meno che la control unit stessa non lo
imponga. Ad esempio, durante la fetch, in una istruzione di salto, la stessa informazione fluisce alla
memoria come se fosse in atto una operazione di load o di store, ma siccome la control unit sa di eseguire
una istruzione di salto, sa che i segnali relativi alle operazioni di load o di store non vanno abilitati.

Pertanto, per realizzare tale soluzione, all’architettura del processore monociclo vanno aggiunti alcuni
dispositivi in grado di salvare lo stato della control unit: ci saranno, cioè, dei registri che rappresentano lo
stato della computazione. Il datapath del multiciclo presenta quindi, rispetto a quello del monociclo, un
registro istruzioni, due registri A e B dopo il register file ed infine un registro per salvare l’output della
computazione, detto ALUOut.

10
Figura 4: Processore multiciclo

Se consideriamo ora una istruzione di load, essa impiega un tempo maggiore rispetto al caso del processore
monociclo perché nel presente caso il flusso informativo deve attraversare dei dispositivi aggiuntivi, ovvero
i registri. Ma ora, quando si presenta una istruzione di salto, è possibile aggiornare il contenuto del program
counter dopo aver computato l’indirizzo, senza attendere altro tempo per passare alla successiva
istruzione. Pertanto, spendiamo più tempo per eseguire una qualsiasi singola istruzione ma, siccome
possiamo variare la frequenza a cui aggiorniamo il PC, il tempo medio per eseguire una sequenza di
istruzioni diventa più piccolo rispetto al processore monociclo. Il completamento dell’esecuzione in tempi
differenti a seconda del tipo di istruzione è una proprietà che sembrerebbe una contraddizione, ma si
spiega con il fatto che l’architettura non è solo un insieme di singoli componenti isolati. Essa, difatti, è una
proprietà che riguarda l’architettura e non i singoli componenti, quindi non la si può spiegare a livello di
componenti ma appunto a livello architetturale.

Anche qui sappiamo che, quando le informazioni iniziano a fluire, il processore deve eseguire operazioni di
fetch e di decode che non dipendono dall’istruzione, in quanto ancora non gli è nota l’istruzione stessa.
Come vediamo nell’automa a stati finiti del multiciclo, nello stato 0 la control unit asserisce differenti
segnali indipendentemente dal tipo di istruzione che andrà ad eseguire. Al successivo colpo di clock, essa
transita nello stato 1 ed asserisce altri segnali, ancora indipendenti dal tipo di istruzione: in particolare,
pone ALUSrcA a 0, in modo da prendere il valore nel PC e porlo in input all’ALU, e poi pone ALUSrcB ad 11,
in modo da prendere l’output della macchina chiamata shift left 2, che prende in input i 16 bit meno
significativi dell’istruzione proveniente dal registro istruzioni, con estensione di segno.

11
Ricordiamo che in questa fase non sappiamo ancora se in questa istruzione i 16 bit meno significativi
contengono qualcosa che dovrà essere addizionato al PC, ma poniamo ugualmente ALUSrcB ad 11, siccome
non possiamo fare nient’altro. Se poi ci servirà, lo avremo già fatto, e avremo risparmiato tempo.
Finalmente, al prossimo colpo di clock la control unit conoscerà l’istruzione da eseguire: il prossimo stato
(stato 2, stato 6, stato 8 oppure stato 9) dipenderà dal codice operativo.

Figura 5: Automa a stati finiti del processore multiciclo

Procediamo, ad esempio, nello stato 2. Siccome ALUSrcA è pari ad 1, prendiamo come input dell’ALU il
contenuto del registro A. Inoltre, con ALUSrcB pari a 10, prendiamo i 16 bit meno significativi dell’istruzione
con estensione di segno, già computati nella fase precedente. Questa operazione è necessaria perché se
l’istruzione da eseguire è una store o una load, per calcolare l’indirizzo di memoria bisogna prendere il
contenuto di un registro (indirizzo base) e addizionarlo all’offset. Siccome l’indirizzo base è il contenuto di
un registro la cui dimensione è 32 bit, mentre l’offset è lungo 16 bit, bisogna eseguire l’estensione di segno
dell’offset prima di addizionare indirizzo base ed offset. Infine, ALUOp è pari a 00 (segnale inviato alla
control ALU): vuol dire che la ALU sommerà i suoi due input e produrrà in output l’indirizzo di memoria
desiderato. Bisogna allora accedere alla memoria per leggere o scrivere a seconda del segnale asserito
(MemRead o MemWrite). Siccome IorD è asserito, si ha che l’indirizzo non dovrà essere preso dal PC: ciò
vuol dire che l’indirizzo è salvato nell’output dell’ALU, quindi deve attraversare un multiplexer ed andare in
memoria. Supponiamo di star eseguendo una load: passiamo allo stato 3 e poi necessariamente allo stato
4, nel quale si va a scrivere nel registro specificato dall’istruzione stessa.

12
Per le altre istruzioni avremo stati differenti. Notiamo che solo alla fine della computazione il sistema
ritorna allo stato di fetch e legge l’indirizzo caricato nel PC. Per eseguire ciò, vediamo che la load impiega 5
cicli di clock, mentre la branch e la jump impiegano 3 cicli di clock. Dunque, rispetto al monociclo, nel
multiciclo aumenta il tempo per eseguire un’istruzione, ma diminuisce il tempo che intercorre tra
l’aggiornamento del PC e l’accesso della control unit al PC. Nel caso precedente, infatti, impiegavamo 5
colpi di clock, nel caso presente invece ne impieghiamo 3. Questo spiega perché l’architettura del multiciclo
è più performante del monociclo.

Pipelining
Abbiamo visto che il processore monociclo è una macchina combinatoria, nella quale l’inizio di
un’istruzione dipende dalla fine dell’esecuzione della precedente istruzione; pertanto, tale approccio è
poco performante. Si può pensare, invece, ad un approccio a “catena di montaggio”, nel quale ad ogni ciclo
di clock inizia un’istruzione, senza attendere il completamento delle altre: questo richiede che il processore
divenga una macchina sequenziale, in grado di conservare lo stato della computazione ad ogni ciclo di
clock. Per fare ciò, bisogna aggiungere degli organi di memoria, ovvero dei registri (inter-stage buffer), fra i
vari dispositivi del datapath che definiscono gli stadi dell’esecuzione. Tali registri garantiscono il
mantenimento ed il fluire dell’informazione da sinistra a destra e, siccome rappresentano dei costi
aggiuntivi e siccome occupano spazio sull’area di silicio, dovrebbero essere i più piccoli possibile; inoltre,
devono essere dei dispositivi master-slave, in modo da essere acceduti in scrittura (sul fronte di salita del
clock) e lettura (sul fronte di discesa del clock) nello stesso colpo di clock.

Figura 6: Processore pipeline

13
Infine, bisogna aggiungere al datapath una control unit in grado di rendere la macchina sequenziale; si può
notare che la control unit invia segnali agli ultimi 3 registri, in quantità “decrescenti” a seconda dello stadio.
La control unit della pipeline è una macchina combinatoria, come quella del monociclo, in quanto ad ogni
ciclo di clock fornisce tutti i segnali che devono essere inviati ai dispositivi per eseguire tutte le operazioni
necessarie a completare l’esecuzione dell’intera istruzione. Dunque, nella pipeline, la singola istruzione
impiega più tempo per essere processata, a causa della presenza dei registri, ma una sequenza di istruzioni
impiega meno tempo ad essere eseguita; la pipeline non aumenta la velocità di esecuzione di istruzioni,
bensì aumenta il throughput, cioè la quantità di istruzioni eseguite nell’unità di tempo.

Si può pensare di aumentare il throughput ulteriormente, rendendo la pipeline più lunga, ovvero
aumentandone gli stadi elementari ed aggiungendo fra di essi degli inter-stage buffer: in questo modo ogni
stadio eseguirà operazioni più semplici ed impiegherà meno tempo. Ad esempio, immaginiamo di avere
una pipeline a 5 stadi, in cui ogni stadio impiega 1 secondo per eseguire l’operazione a cui è demandato;
essa avrà un throughput di 1 istruzione al secondo. Se raddoppiamo gli stadi, avremo una pipeline a 10
stadi, in cui ogni stadio impiega 1/2 secondo per eseguire l’operazione; in tal caso, avremo un throughput
di 2 istruzioni al secondo. Il problema di tale schema è che esso considera:

Mentre il modello di Von Neumann considera:

Dove è il tempo di inizio dell’istruzione numero 1, è il tempo di inizio dell’istruzione numero 0, è il


tempo per eseguire l’intera istruzione 0, infine è il tempo per eseguire uno degli stadi in cui è suddivisa
l’esecuzione dell’istruzione 0. Ora, siccome stiamo eseguendo una sequenza di istruzioni, ovvero un
algoritmo, l’istruzione numero 1 potrebbe dipendere dall’output dell’istruzione numero 0 e, dopo un certo
tempo , tale output potrebbe non essere ancora pronto.

Quindi, abbiamo appreso che il trucco della pipeline è iniziare il processamento di un’istruzione senza
attendere che l’istruzione precedente sia conclusa; questo in un certo modo viola il modello di Von
Neumann. Per rispettare il modello, perciò, la pipeline deve avere un vincolo, ovvero garantire che l’output
prodotto dall’implementazione della pipeline corrisponda all’output prodotto da una macchina di Von
Neumann sotto qualsiasi circostanza, senza eccezioni. Dobbiamo cioè assicurarci che la pipeline non utilizzi
mai valori incoerenti dei registri, sia durante sequenze di istruzioni sia in istruzioni di salto. In tali situazioni,
affinché le istruzioni utilizzino valori coerenti dei registri, esse non possono essere eseguite in cicli di clock
successivi: questi eventi vengono chiamati hazard.

Pipeline hazard
Un hazard è una situazione in cui la pipeline non può eseguire un’istruzione ad un determinato ciclo di
clock. Gli hazard si dividono in 3 tipologie:
Hazard strutturali, causati dal fatto che l’hardware non supporta la combinazione di istruzioni che si
desidera eseguire nello stesso colpo di clock. Si pensi, ad esempio, al voler accedere in lettura e
scrittura, durante lo stesso colpo di clock, ad una memoria comune.
Hazard dei dati, che si verificano quando la pipeline deve essere messa in stallo perché l’esecuzione
di un’istruzione dipende dal completamento di un’altra istruzione.
Hazard di controllo o di branch, causati dal fatto che un’istruzione comporta un salto e dunque le
istruzioni che la seguono nella pipeline non sono quelle corrette.

14
Gli hazard strutturali in genere si risolvono nel modo più ovvio, cioè dando la precedenza all’istruzione che
è più vicina al termine dell’esecuzione.

Gli hazard dei dati possono essere risolti in due modi principali:
Eliminando le dipendenze fra i dati; non sempre è possibile ed è un compito demandato al
compilatore, il quale riscrive l’algoritmo (non il codice) in modo coerente.
Introducendo la tecnica del forwarding o bypassing. Essa si basa sul fatto che in genere non
abbiamo bisogno che un’istruzione sia completa per poter usufruire del suo output: aggiungendo
un dispositivo in più, definito forwarding unit, ci sarà possibile prelevare il dato necessario al
completamento della maggior parte delle istruzioni senza mettere in stallo la pipeline. In alcuni
casi, però, mettere in stallo la pipeline è l’unico modo per rendere coerente il contenuto dei registri
fra diverse istruzioni, per fare ciò si introduce un codice operativo definito “nop” (no operation).

Infine, gli hazard di controllo possono essere risolti in vari modi, tra i quali:
Mettendo in stallo la pipeline. Difatti, in una istruzione di branch si ha che al primo colpo di clock si
preleva l’istruzione dalla memoria, al secondo si riconosce che è un salto, al terzo si computa la
condizione di salto ed al quarto colpo di clock si preleva l’indirizzo a cui saltare. Dunque, si
potrebbe mettere in stallo la pipeline appena si riconosce che l’istruzione è un salto, senza sapere
se è verificato o meno, ma questo sprecherebbe 4 cicli di clock. Si può arrivare a perdere al
massimo 2 cicli di clock, aggiungendo un dispositivo ulteriore che permetta di aggiornare il program
counter al quarto colpo di clock. In ogni caso, però, un salto introduce una penalizzazione delle
prestazioni, in quanto comporta la perdita di alcuni cicli di clock (penalty).
Utilizzando la tecnica della prediction. Vi sono varie tecniche di prediction, le più elaborate
utilizzano un approccio che si basa sul comportamento delle precedenti istruzioni di salto.
La tecnica di prediction più semplice suppone che, ogniqualvolta vi sia un’istruzione di branch, essa
non sia mai verificata, di modo che l’esecuzione possa continuare senza la perdita di cicli di clock.
Tale tecnica è abbastanza accurata in quanto le istruzioni di salto si trovano in presenza di cicli;
realizzando il compilatore che utilizzi cicli while con condizioni sempre verificate (anziché cicli
repeat), quindi, i salti non avverranno mai.
È possibile anche decidere la predizione per ciascun salto in funzione del comportamento
precedente di quell’istruzione di salto, durante l’esecuzione del programma: questo tipo di
predizione è detta predizione dinamica. Essa può essere implementata memorizzando il passato di
ciascun salto, ricordando quando si è verificato o meno per predire il suo futuro comportamento.
Aggiungendo un ulteriore dispositivo, definito branch target buffer (BTB). Esso è una cache in cui
ogni entry ha due campi: nel campo “tag” verrà salvato l’indirizzo dell’istruzione di salto, nel campo
“dati” il corrispondente indirizzo a cui saltare. Quindi, la prima volta che si incontra una certa
istruzione di branch, si alloca una entry nel BTB: dalla seconda volta che si incontra quella stessa
istruzione di branch, il BTB permetterà di avere a disposizione l’istruzione successiva, senza avere
alcuna penalty.

15
Interruzioni ed Eccezioni
Come abbiamo visto, il ciclo del processore è costituito dalle seguenti fasi:

repeat
fetch
decode
execute
memory
write-back
if int
interrupt
forever

Andiamo in tale contesto a distinguere due tipi di eventi “non ordinari” che possono verificarsi durante
l’esecuzione del ciclo del processore:
Interruzioni (interrupt): eventi che interrompono la normale esecuzione di un’istruzione, quando la
condizione “if int” è verificata. Rappresentano circostanze in cui la normale esecuzione deve
essere sospesa perché il processore deve sincronizzarsi con l’evoluzione di un altro agente, come
ad esempio quando si riceve un input da una tastiera o da altri dispositivi di I/O.
Eccezioni (exception): eventi che ostacolano la normale esecuzione di un’istruzione, durante
qualsiasi fase del ciclo del processore. Rappresentano circostanze che segnalano problemi
nell’esecuzione di un’operazione, causati dal fatto che i valori computati non sono ammessi nel
dominio dell’operazione, come ad esempio un page fault o una divisione per zero.

Va detto comunque che la definizione di questi eventi dipende generalmente dal tipo di processore che
andiamo a considerare: più genericamente, si possono considerare le eccezioni come un tipo particolare di
interruzioni.

Interruzioni
Supponiamo ad esempio che il processore stia eseguendo un programma che riceve alcuni input dalla
tastiera. In questo caso, il processore non può eseguire correttamente il programma finché i valori di input
prelevati dalla tastiera non saranno disponibili nella “source destination” (che è una porzione di memoria o
un certo numero di registri, a seconda del tipo di implementazione del processore). Quando essi saranno
disponibili, la tastiera genererà la cosiddetta interrupt request (IRQ), ovvero un segnale che verrà inviato
alla CPU per notificare una richiesta di interruzione. Perciò, bisognerà sincronizzare l’evoluzione del
programma con l’evoluzione della tastiera di modo che, quando il programma raggiunge il punto in cui deve
prelevare i dati da tastiera, il contenuto della “source destination” è ciò che il programma si aspetta. In
questo caso, il contenuto della “source destination” deve essere una stringa di bit che rappresenta i tasti
premuti, perciò il programma deve accertarsi che sia avvenuta la digitazione da tastiera e quindi deve
aspettare finché non arrivi la IRQ. Questa attesa nel programma si può ottenere con un ciclo che verifica lo
stato del dispositivo di input finché esso non indica che il dispositivo è pronto. Tale tecnica, definita polling,
non è efficiente perché lascia il processore in attesa dell’input senza fare altro, e ciò comporta uno spreco
di cicli di clock.

16
Vorremmo che il processore recuperi questo tempo facendo qualcos’altro mentre aspetta: bisogna quindi
fermare l’esecuzione di quel programma, eseguire altro (cambiando il contenuto del program counter) e
poi tornare al programma quando l’input è pronto.

A tale scopo esiste il meccanismo detto interrupt servire routine (ISR). L’ISR o gestore di interrupt è una
funzione la cui esecuzione è innescata dal sistema operativo in risposta al verificarsi di un’interruzione.

In particolare, quando arriva un IRQ, la CPU deve mandare in esecuzione l'ISR predisposta ad hoc per quello
specifico interrupt. Affinché il meccanismo funzioni correttamente, è necessario che tutte le azioni svolte
dall'ISR siano trasparenti rispetto al programma interrotto. Questo vuol dire che al termine della routine
deve essere ripristinato tutto come era prima dell'interruzione. Per fare ciò, prima di mandare in
esecuzione l'ISR, il processore deve effettuare una commutazione di contesto (context switch), ovvero
salvare l’attuale contenuto del program counter, in modo da potervi ritornare alla fine dell’ISR.

Può, ancora, capitare che due o più agenti vogliano sincronizzarsi con il processore, ad esempio la tastiera e
la stampante, generando in contemporanea più di una IRQ. In questo caso, la CPU deve avere un criterio
per decidere nel minor tempo possibile quale interruzione servire per prima. A tale scopo, può essere
presente un ulteriore pezzo di memoria, definito look-up table, usato per sostituire operazioni di calcolo
a runtime con una più semplice operazione di consultazione. Essa infatti associa una lista di ISR alle loro
corrispondenti IRQ. Tale dispositivo è un componente hardware in più, dunque allunga il ciclo di clock
(perché bisogna attraversare una macchina in più) ed aumenta il costo complessivo dell’implementazione,
ma apporta un vantaggio notevole, legato al fatto che il sistema operativo sarà in grado di gestire in modo
più semplice, efficiente e veloce gli interrupt.

Le interruzioni possono essere di due tipologie:


Interruzione precisa (precise interrupt), permette alla CPU di produrre un output equivalente
all’output che la CPU darebbe se fosse monociclo. In pratica, in un’interruzione precisa, tutte le
istruzioni che precedono l’interrupt (in ordine architetturale) vengono completate, mentre tutte
quelle che la seguono vengono annullate con delle nop. Ciò rende sempre coerente lo stato della
computazione con il tempo in cui avviene l’interrupt, ma causa uno spreco di cicli di clock ed
aggiunge costi relativi alla complessità della control unit.
Interruzione imprecisa (imprecise interrupt), permette alla CPU di completare l’esecuzione di tutte
le istruzioni in atto prima di servire l’interrupt. In pratica, in un’interruzione imprecisa, l’esecuzione
dell’interruzione è traslata nel tempo rispetto a quando essa deve realmente iniziare. In tal caso,
l’ISR deve essere in grado di fare previsioni sullo stato della computazione, per evitare situazioni in
cui esso possa essere incoerente rispetto al tempo in cui avviene l’interruzione.

Interruzioni precise
Un’interruzione che lascia la macchina in uno stato ben definito è detta interruzione precisa. Essa presenta
le seguenti caratteristiche:
Il contenuto del PC è salvato in un preciso registro (detto EPC, come vedremo in seguito) per poter
riprendere la computazione al termine dell’interruzione
Tutte le istruzioni precedenti a quella puntata dal PC vengono completate
Tutte le istruzioni successive a quella puntata dal PC vengono annullate
Lo stato dell’esecuzione dell’istruzione puntata dal PC è conosciuto e salvato per poter riprendere
la computazione
Un’interruzione che non gode di tali proprietà è definita imprecisa.

17
Eccezioni
Le eccezioni sono eventi che si oppongono al completamento delle istruzioni che sono in esecuzione nel
processore. Siccome un’eccezione è un evento molto specifico, sappiamo identificare quale stato della CPU
abbia generato tale evento. In particolare, in presenza di un’istruzione che genera un’eccezione, dobbiamo
far ripartire tale istruzione garantendo che essa venga eseguita come se l’eccezione non fosse avvenuta.
Supponiamo ad esempio che il processore stia eseguendo un’istruzione di load, durante la quale si verifica
un page fault; in questo caso, il processore deve risolvere il problema e permettere l’esecuzione della load,
garantendo che essa produca lo stesso effetto che avrebbe prodotto se non ci fosse stato un page fault. Si
evince quindi che la gestione delle eccezioni nelle istruzioni di memoria è un problema di cooperazione tra
il processore e la MMU.

Vediamo quali sono le tipologie di eccezioni in relazione alle fasi del ciclo del processore:
Page fault (pf) nella fase di fetch. Si verifica quando non c’è un mapping, ovvero una
corrispondenza, fra la memoria fisica e l’indirizzo virtuale generato dal processore.
Invalid operation code (inv. opcode) nella fase di decode. Si verifica quando c’è un mismatch,
ovvero una mancata corrispondenza, fra il codice operativo di un’istruzione e l’istruzione da
eseguire. Questo può accadere quando il numero di istruzioni che la macchina può eseguire è
minore del numero di rappresentazioni possibili delle istruzioni. Ad esempio, se il codice operativo
è rappresentato con 6 bit, avremo 26 rappresentazioni possibili delle istruzioni; quindi se abbiamo
un numero di istruzioni che è minore di 26, vorrà dire che alcune rappresentazioni saranno prive di
senso e causeranno eccezioni. Una soluzione potrebbe essere, anziché codificare tutte le istruzioni,
codificare classi di istruzioni, così come fa il MIPS.
Errori matematici nella fase di execute. Si verificano quando si effettuano operazioni logico-
aritmetiche che generano valori privi di senso. Ad esempio, se effettuiamo addizioni e
moltiplicazioni tra valori troppo grandi rispetto alla dimensione dei registri a disposizione,
otterremo risultati privi di senso. A tale scopo, è presente un dispositivo hardware, definito
program status register, il quale è un registro contenente vari flag utili alla CPU per indicare lo
stato e la correttezza di differenti operazioni matematiche. Abbiamo in particolare due tipi di flag
(flag di condizione e flag di controllo). Alcuni dei flag di condizione sono:
o ZF (zero flag), indica se il risultato di un'operazione matematica o logica è zero
o CF (carry flag), indica se il risultato di un'operazione produce una risposta non contenibile
nei bit usati per il calcolo.
o OF (overflow flag), indica se il risultato di un'operazione è in overflow, secondo la
rappresentazione in complemento a due
o PF (parity flag), vale 1 se il numero di 1 negli ultimi 8 bit meno significativi del risultato è
pari, 0 altrimenti
Page fault (pf) nella fase di memory. Come nella fase di fetch, si verifica quando non c’è un mapping
fra la memoria fisica e l’indirizzo virtuale generato dal processore.
Protection issue (pi) nella fase di write-back. Si verifica quando si tenta di scrivere in registri in cui
non è possibile scrivere perché essi devono contenere costanti di uso frequente (ad esempio il
registro $0). Questo stesso meccanismo è utilizzato dal sistema operativo per evitare la scrittura in
alcune aree di memoria private, ovvero dedicate a determinati programmi.

18
Abbiamo in particolare due livelli di protezione:
o Il primo livello di protezione è dato dalla memoria virtuale, la quale si assicura che non ci
sia alcuna pagina fisica che corrisponde a due pagine virtuali o a due processi differenti.
Nella memoria virtuale, infatti, la page table tiene memoria di quali processi accedono a
determinate aree di memoria.
o Il secondo livello di protezione è dato dal meccanismo detto guard violation mode, il quale
riguarda due registri nella process description table. Quest’ultima è una tabella che
descrive e caratterizza un processo, dunque ogni processo ha la sua process description
table. Essa contiene tutte le informazioni che devono essere salvate quando la CPU passa
da un processo ad un altro in un sistema multi-tasking. Le informazioni contenute nella
tabella permettono al processo di essere sospeso e poi ripreso dal punto in cui è stato
sospeso. In particolare, nella process description table vi sono due registri (definiti
“guards”), i quali indicano i limiti della memoria dedicata al processo, impedendo al
processo di intaccare altre aree di memoria.

Gestione delle interruzioni


Nel presente paragrafo, si tratteranno le soluzioni architetturali per la gestione delle interruzioni,
considerando le eccezioni come un caso specifico di interruzioni; perciò tutte le soluzioni architetturali
proposte sono valide anche per la gestione delle eccezioni.

Come abbiamo detto, quando un’istruzione genera un’interruzione, il sistema operativo deve gestire
l’interruzione e poi far ripartire l’esecuzione dell’istruzione come se l’interruzione non fosse avvenuta. A
tale scopo, bisogna aggiungere due componenti hardware alla pipeline, ovvero l’EPC ed il registro Causa.

Innanzitutto, la CPU deve essere in grado di salvare l’indirizzo dell’istruzione che ha generato l’interruzione
in un registro detto EPC (exception program counter). Fatto questo, il sistema operativo deve caricare nel
program counter l’indirizzo della prima istruzione dell’ISR. Infine, al termine delle operazioni dell’ISR,
l’esecuzione deve ripartire da dove è stata interrotta, ovvero dal contenuto di EPC, che di fatto è un
indirizzo di ritorno indispensabile.

Va detto inoltre che l’ISR si configura come un’insieme di istruzioni del tipo:

case(causa_1)
do…
case(causa_2)
do…
case(causa_3)
do…

Quindi, se si verifica una certa interruzione con una determinata causa, essa ricadrà ad esempio nel
case(causa_1)e verrà gestita con una determinata sequenza di passi. Ancora, se si verifica un’altra
interruzione, derivante da un’altra causa, essa ricadrà ad esempio nel case(causa_2), che dà l’avvio ad
un’altra sequenza di passi per gestire questo tipo di interruzione. In conclusione, vi sono tanti case quante
sono le interruzioni: ogni case è il primo di una serie di passi finalizzati a gestire una certa interruzione.

19
Dunque, la CPU deve fornire al sistema operativo anche la causa dell’interruzione, affinché esso possa
trasferire il controllo dell’esecuzione alla prima istruzione dell’ISR. Ciò è possibile ad esempio con un
registro dedicato, detto registro Causa (implementato nel program status register), finalizzato a
memorizzare la causa dell’interruzione. Esso associa una differente stringa di bit ad ogni causa. L’ISR,
quindi, dovrà scorrere tutti i case fino a trovare il case corrispondente alla causa fornita dalla CPU. Va
detto che il registro Causa vede in ingresso su alcuni dei suoi flip-flop dei segnali provenienti dall’esterno:
sono le cosiddette IRQ o richieste di interruzioni.

Interruzioni vettorizzate
Esiste anche un altro modo per comunicare la causa dell’ interruzione al sistema operativo, esso consiste
nel metodo delle eccezioni vettorizzate. In questo caso, l’indirizzo da caricare nel program counter è
determinato dalla causa stessa. Praticamente, tale indirizzo è ottenuto direttamente dal contenuto del
registro Causa, grazie ad una tavola di look-up (look-up table). Quest’ultima associa ogni causa al
corrispondente indirizzo dell’istruzione dell’ISR che serve tale causa.

In questo caso, complicando l’hardware, si riduce l’overhead associato al fatto che l’ISR dovesse scorrere
tutti i case per determinare le operazioni da eseguire. Basta fare in modo che gli indirizzi della tavola di
look-up siano spiazzati di k, dove k è la potenza di 2 più prossima al massimo numero di istruzioni richieste
da un’operazione dell’ISR per gestire una certa interruzione. Ad esempio, se nell’ISR l’operazione più lunga
richiede 13 istruzioni, si pone il valore di k a 16: in questo modo, per passare da un indirizzo al successivo
bisogna spostarsi di 16. Questo significa altresì che non è necessario l’intero indirizzo ma solo i suoi bit
meno significativi a partire da quello di peso 16.

Gestione della priorità delle interruzioni


Può capitare che nello stesso momento possano verificarsi eventi che sollevano più di un’interruzione.
In tal caso, si stabilisce un criterio di priorità.
Una soluzione consiste nell’ordinare le cause in ordine di priorità crescente all’interno della tavola di look-
up: la causa a priorità massima andrà all’indirizzo 0, la seconda all’indirizzo 16, la terza all’indirizzo 32 e così
via fino all’ultima, che avrà priorità minima ed indirizzo k. Tale soluzione è però poco efficiente in quanto
presuppone la memorizzazione di tutte le cause.

Un altro metodo consiste nell’introduzione di una rete di priorità. La rete di priorità è una rete
combinatoria descritta da un numero di ingressi X e di uscite Y pari al numero di cause.
Nello specifico, gli ingressi sono caratterizzati da un ordine di priorità crescente, inoltre ogni uscita è alta se
il corrispondente ingresso è alto e tutti i precedenti ingressi sono bassi. In sintesi:
Y1 = X1
Y2 = X2 AND NOT(X1)
Y3 = X3 AND NOT(X1) AND NOT(X2)

Per costruzione, quindi, la rete di priorità restituisce solo un’uscita alta, ovvero quella più prioritaria.
Possiamo inoltre aggiungere un transcodificatore a valle della rete di priorità, in modo da eliminare la
necessità della tavola di look-up. Il transcodificatore è una rete combinatoria che prende in ingresso le
uscite della rete di priorità e restituisce un’unica uscita U. Questa dipende dalla causa ed ha due funzioni:
Codifica il valore di priorità dell’interruzione (la priorità massima ha valore 0)
Rappresenta lo spiazzamento (rispetto all’indirizzo base dell’ISR) dell’indirizzo dell’istruzione
dell’ISR necessaria a gestire l’interruzione

20
Dunque, bisognerà inserire nel registro di stato anche il valore della priorità del task che è in esecuzione.
Quando arriva un’interruzione, basterà confrontare la sua priorità con il valore della priorità del task in
esecuzione, attraverso un comparatore. In questo modo, ci si assicura che il processore venga interrotto
solo se tra le cause di interruzione ce n’è una che è più prioritaria rispetto al task in esecuzione; inoltre, si
evita che l’ISR parta in automatico ad ogni interruzione, anche a quelle con priorità basse.

In tale contesto, innanzitutto bisogna notare che l’assegnazione “statica” delle priorità non sempre
rispecchia le reali esigenze dei processi. Inoltre, bisogna evitare alle interruzioni a priorità alta di
monopolizzare l’ISR, impedendo a tutte le altre interruzioni di essere servite.
Per evitare ciò, ci sono due tecniche principali:
La tecnica del round-robin, la quale tiene memoria della causa che ha sollevato l’ultima
interruzione servita. In tale modo, quando arriva la prossima interruzione, la serve soltanto se la
sua priorità è maggiore rispetto alla priorità dell’interruzione precedente. Questo garantisce una
certa equità tra tutte le possibili interruzioni.
La variazione dinamica delle priorità, che è generalmente usata dai processori.
Essa consiste nel variare dinamicamente il livello di priorità di un processo mentre è in esecuzione,
in base a due fattori:
o Frequenza di accadimento del processo
o Durata del processo
Grazie ad essa, si tende ad aumentare la priorità dei processi che stanno per terminare; essi si
riconoscono per il principio di località: più il PC è lontano dal primo indirizzo del programma, più il
programma si sta avvicinando alla fine.

In conclusione, possiamo rimarcare che le eccezioni e le interruzioni vengono servite attraverso lo stesso
hardware presentato in questo capitolo. Vi è inoltre un meccanismo di priorità più ampio: le eccezioni
vengono servite prima delle interruzioni per ovvi motivi di tempistica. Per questo motivo, il registro Causa
presenterà alcuni bit dedicati alla gestione delle eccezioni e quindi pilotati da segnali interni ed altri bit
dedicati alla gestione delle interruzioni e quindi pilotati da segnali provenienti dall’esterno (IRQ).

Processore multiciclo con la gestione delle interruzioni


Come abbiamo visto nei paragrafi precedenti, il supporto minimo che l’hardware fornisce al sistema
operativo, che nella fattispecie è l’ISR, per gestire le interruzioni e le eccezioni è rappresentato dal registro
EPC e dal registro Causa. È fornito inoltre anche l’indirizzo costante da salvare nel PC, il quale è l’indirizzo
della prima istruzione dell’ISR. Si ricorda che, in tale contesto, indichiamo con il termine “interruzioni”
anche le eccezioni.

In un processore multiciclo, allora, sono necessari due segnali di controllo aggiuntivi per pilotare la scrittura
dei registri EPC e Causa, detti rispettivamente EPCWrite e CauseWrite; inoltre serve un segnale di controllo
di m bit, detto IntCause, per abilitare un multiplexer a 2m vie (dove 2m è il numero di interruzioni possibili).
L’output di tale multiplexer va a settare in modo appropriato i bit meno significativi del registro Causa.

in più, occorre poter scrivere nel PC l’indirizzo iniziale dell’ISR. Nel classico multiciclo, il PC è alimentato
dall’uscita di un multiplexer a 3 vie, controllato dal segnale PCSource: basterà trasformarlo in un
multiplexer a 4 vie, con l’ingresso aggiuntivo collegato al valore costante dell’indirizzo della prima istruzione
dell’ISR.

21
Infine, siccome il PC viene incrementato nel primo ciclo di ciascuna istruzione, non si può semplicemente
salvare il valore di PC in EPC: tale valore è pari all’indirizzo desiderato più 4. Pertanto, bisogna utilizzare la
ALU per effettuare la sottrazione PC-4 e scriverne il risultano in EPC: questo non richiede nessun segnale
aggiuntivo, poiché la ALU può essere usata per sottrarre ed ha già in ingresso la costante 4. Allora,
l’ingresso di EPC prende l’uscita dell’ALU.

Nella figura seguente, vediamo il processore multiciclo modificato per supportare la gestione delle
interruzioni. Si precisa che si suppone per semplicità che esistano solo due tipi di interruzioni:
Istruzione indefinita (invalid operation code), si verifica quando c’è una mancata corrispondenza fra
il codice operativo di un’istruzione e l’istruzione da eseguire
Overflow aritmetico, indica che il risultato di un'operazione è in overflow
Per tale motivo, il segnale IntCause sarà un segnale di un bit (che vale 0 per l’operazione invalida ed 1 per
l’overflow) ed il multiplexer che andrà ad abilitare sarà un multiplexer a due vie.

Figura 7: Processore multiciclo con la gestione delle interruzioni

La figura successiva, in più, rappresenta l’automa a stati finiti che descrive il processore multiciclo capace di
gestire il verificarsi dei due tipi di interruzioni appena menzionate. Rispetto all’automa a stati finiti del
classico multiciclo, tale versione presenta due stati in più (in basso a destra nella figura), relativi
rispettivamente all’overflow e all’istruzione indefinita. Il primo viene raggiunto nel caso in cui la fase di
esecuzione dichiara appunto un overflow, il secondo invece viene raggiunto quando la fase di decode
decodifica un codice operativo invalido.
Si noti che possono verificarsi dei problemi se consideriamo tale automa, i quali vanno risolti con ulteriori
operazioni di controlli. Nel caso di overflow, ad esempio, l’overflow viene riconosciuto a valle della fase di
esecuzione, che dunque ha prodotto e scritto un risultato errato.

22
Occorre, a tale scopo, disattivare la scrittura nei registri. Per fare ciò nel caso di overflow basta mettere in
AND con RegWrite il segnale Overflow negato:
RegWrite = RegWrite AND NOT(Overflow)

Figura 8: Automa a stati finiti del processore multiciclo con la gestione delle interruzioni

Infine, se si vogliono aggiungere altri tipi di interruzione, basterà inserire tanti nuovi stati (come quelli in
basso a destra nella figura sovrastante) quante sono tali interruzioni, con le opportune operazioni di
controllo. Questi stati sono caratterizzati tutti dagli stessi tipi di operazioni (CauseWrite, EPCWrite,
PCWrite, ecc.) ma da differenti valori di IntCause, in quanto corrispondono alla scrittura di differenti valori
nel registro Causa. Questi valori verranno poi valutati dall’ISR per decidere le operazioni da eseguire. Si noti
che c’è un problema nell’aggiunta di questi nuovi stati, legato all’allungamento dei tempi di esecuzione.

Per concludere, va detto che possiamo anche implementare la gestione delle interruzioni vettorizzate:
in ingresso al multiplexer a 4 vie non va messo l’indirizzo fisso dell’ISR, bensì un indirizzo che dipende dalla
causa, come abbiamo visto nei paragrafi precedenti.

23
Processore pipeline con la gestione delle interruzioni
In figura è mostrato il processore pipeline modificato per la gestione delle interruzioni. Si precisa che si
suppone per semplicità che esistano solo due tipi di interruzioni:
Istruzione indefinita (invalid operation code)
Overflow aritmetico
Rispetto alla classica pipeline, bisogna innanzitutto modificare il multiplexer nella fase di decode, in modo
che sia controllato non solo dal segnale generato dall’hazard detection unit (per gestire i salti) ma anche
dalla control unit, tramite il segnale ID.Flush, che indica di mettere in stallo la pipeline nel caso di istruzione
indefinita. Questi due segnali vanno in ingresso a una OR, la cui uscita seleziona la via 1 del multiplexer, la
quale azzera i segnali WB, MEM e EX delle fasi successive.
Bisogna procedere allo stesso modo nel caso di overflow aritmentico: la ALU deve asserire il segnale di
overflow alla control unit prima del fronte di salita del clock, in modo che il risultato dell’ALU non venga
scritto nell’interstage buffer EX/MEM. La control unit, allora, invierà il segnale EX.Flush ai due multiplexer
in alto nella fase di execute, in modo da mettere in stallo la pipeline dallo stadio EX in poi.

Figura 9: Processore pipeline con la gestione delle interruzioni

Si noti altresì la posizione dei registri EPC e Causa nella pipeline. Il fatto che il registro Causa non sia
collegato a nulla significa che esso può essere acceduto da qualunque stadio.
Il registro EPC, invece, è posto due colpi di clock dopo la fase di fetch, quindi in esso va salvato il valore
PC+4. Ciò significa che bisognerà sottrarre 4 a tale valore, per ottenere il corretto indirizzo da inserire nel
PC alla fine dell’interruzione. Questo può essere fatto in hardware o in software, nel secondo caso è l’ISR
che se ne occupa. Il registro EPC, inoltre, è posto in quel punto perché la fase di decodifica deve prima
decodificare l’eventuale istruzione indefinita per poterla notificare.
Infine, se volessimo aggiungere l’interruzione riguardante i page fault, si deve tener conto del fatto che la
memoria deve restituire un segnale che indica se si è verificato un page fault. Si deve anche scegliere se
dotare l’hardware di componenti che permettano di aggiornare il PC con l’istruzione dell’ISR prima della
fine della fetch oppure nella fase di decode.

24
La Gerarchia di Memoria
Problematiche principali
In un processore, una delle caratteristiche più importanti è il tempo di esecuzione delle istruzioni:
vorremmo che il processore sia veloce, dunque vorremmo che i tempi di esecuzione siano brevi.
In particolare sappiamo che, tra le differenti operazioni eseguibili dal processore, quelle cruciali riguardano
l’accesso in memoria, il quale richiede tempi più lunghi rispetto alle altre operazioni.

L’accesso in memoria (intesa come memoria istruzioni e memoria dati) dipende dalla tecnologia con cui è
realizzata la memoria stessa; tale tecnologia pone un limite inferiore ai tempi di accesso. Nello specifico, il
tempo di decodifica di un indirizzo (che in linea di principio dovrebbe essere indipendente dalla lunghezza
dell’indirizzo) aumenta all’aumentare della dimensione della memoria (e dunque all’aumentare della
lunghezza dell’indirizzo): questa è una indiretta implicazione della tecnologia stessa. Si potrebbe pensare di
ridurre i tempi di decodifica dell’indirizzo facendo in modo di ridurre la dimensione dell’indirizzo. L’idea
potrebbe essere di considerare la memoria come una matrice, ovvero un array bidimensionale anziché
unidimensionale; ogni suo elemento avrebbe quindi un indice di riga ed un indice di colonna. Tali indici
potrebbero essere facilmente derivati dall’indirizzo: si potrebbe cioè strutturare l’indirizzo in due metà e, se
si vuole accedere per righe, si potrebbe far sì che la prima metà rappresenti l’indice di colonna mentre la
seconda metà rappresenti l’indice di riga. A questo punto, si dovrebbe mandare in AND l’output dei due
decoder per trovare il corrispondente elemento della matrice, quindi bisognerebbe aggiungere più
hardware. Si potrebbe, ancora, iterare il procedimento per rendere la memoria un array n-dimensionale, in
modo da ridurre la lunghezza dell’indirizzo e di conseguenza il tempo di decodifica dello stesso: questo però
trova limitazioni legate alla dimensione del circuito e al fan-in delle porte logiche.

Come dicevamo, l’accesso in memoria richiede tempi più lunghi, questo è vero per differenti motivi.
Uno di essi è legato al fatto che la memoria non risiede nello stesso circuito del processore per questioni di
spazio e modularità, difatti si desidera che la memoria sia molto grande ed eventualmente sostituibile.
Inoltre, la memoria è una macchina sequenziale che evolve in accordo ad un suo clock, il quale dipende
dalla tecnologia ed è demandato a temporizzare le operazioni di lettura e scrittura in memoria.
Infine, per questioni di costo e dimensione, la memoria è realizzata in DRAM (memoria dinamica), quindi è
più lenta e necessita di periodici refresh, che impiegano un certo tempo. Pertanto, quando la memoria è
occupata ad effettuare un refresh, dal punto di vista del processore l’attuale ciclo di clock è più lungo,
perché tiene conto del fatto che la memoria è occupata.

A proposito di ciò, si potrebbe pensare di realizzare la memoria con la stessa tecnologia degli inter-stage
buffer (memoria statica SRAM, che mantiene l’informazione finché è fornita alimentazione al dispositivo)
per renderla più veloce, ma in tal caso essa sarebbe estremamente costosa ed ingombrante e, pertanto, si
sceglie di realizzarla in DRAM (memoria dinamica). Infatti, un flip-flop D (memoria dinamica) può essere
realizzato con un solo transistor, mentre per implementare una cella di memoria statica si necessita di 6
transistor. Le memorie dinamiche sono più semplici ed economiche, la singola cella di memoria dinamica
occupa poco spazio e quindi rende possibile avere molte celle su un’area di silicio. Il problema principale di
una memoria dinamica è che non mantiene l’informazione fino allo spegnimento del dispositivo, bensì
necessita di periodici refresh (lettura e riscrittura dei dati). Questo vuol dire che, durante il refresh, la
memoria non è disponibile per nessun’altra operazione.

25
Si può notare che progettare il timing dell’operazione di refresh è cruciale: renderla frequente assicura di
non perdere informazioni ma aumenta la probabilità di trovare la memoria occupata.

Possiamo quindi asserire che il tempo per effettuare l’operazione di fetch è più lungo del tempo necessario
ad effettuare l’operazione di decode, perché la prima operazione accede alla memoria indirizzi mentre la
seconda accede al register file, il quale per costruzione è più piccolo della memoria indirizzi e risiede nello
stesso circuito del processore, a differenza della memoria indirizzi. Per gli stessi motivi, si può dire che il
tempo dell’operazione di memory è più lungo del tempo di write back. Si nota inoltre che il tempo di
execute si dimensiona sul tempo dell’operazione più lunga, anche se, come sappiamo, differenti operazioni
logico-aritmetiche necessitano di tempi differenti per essere eseguite.

In ultima analisi, allora, le prestazioni del processore possono essere migliorate riducendo i tempi di
accesso in memoria e di esecuzione delle operazioni logico-aritmetiche (ovvero i tempi di fetch, memory ed
execute). I tempi di fetch e di memory si diminuiscono introducendo la gerarchia di memoria ed il caching,
mentre il tempo di execute si diminuisce introducendo un pipelining nello stadio di execute (si parlerà di
processori scalari e superscalari, in essi si può pensare alla ALU come ad un array di macchine che eseguono
differenti operazioni nello stesso istante).

Livelli di memoria
Si introduce il principio di località, il quale ci permetterà di creare l’illusione di una memoria grande e
veloce. Il principio di località afferma che i programmi accedono ad una porzione relativamente piccola del
loro spazio degli indirizzi ad ogni istante di tempo. Ci sono due tipi di località:
Località temporale, la quale afferma che, se un oggetto è referenziato, tenderà ad essere presto
referenziato di nuovo
Località spaziale, la quale ci dice che, se un oggetto è referenziato, gli oggetti i cui indirizzi sono
vicini ad esso tenderanno ad essere presto referenziati

I parametri da considerare per una memoria sono la velocità, la dimensione ed il costo ($/bit): le SRAM, ad
esempio, sono veloci, piccole e costose mentre i dischi magnetici sono lenti, grandi ed economici; infine, le
DRAM sono una via di mezzo tra SRAM e dischi magnetici.
La soluzione per conciliare tali parametri è basarsi sul principio di località per introdurre una gerarchia di
memoria, ovvero strutturare una memoria a livelli: il livello più basso sarà il livello più vicino al processore
(memoria cache), quello più alto sarà il più lontano dal processore (memoria virtuale); tra questi due ci sarà
un livello intermedio (memoria fisica). Come è ovvio pensare, più il livello di memoria è lontano dal
processore e più aumentano i tempi per accedervi e la sua dimensione.
I livelli di memoria sono i seguenti:
Memoria cache, che è il livello di memoria più basso e più vicino al processore. Essa è una piccola
memoria implementata in SRAM nello stesso chip del processore per ridurre i tempi di accesso ed
evitare il refresh. La memoria cache contiene la copia di una porzione della memoria fisica;
vorremmo che essa contenga sempre i dati desiderati, per non andare in memoria fisica.
Memoria fisica o principale, che è il livello di memoria intermedio. Essa è la memoria cui fa
riferimento il modello di Von Neumann ed è in genere implementata in DRAM. La memoria fisica
contiene la copia di una porzione della memoria virtuale.
Memoria virtuale, che è il livello più alto e più lontano dal processore. Essa è una memoria grande e
lenta, su disco magnetico ed organizzata in blocchi ordinati della stessa dimensione, detti pagine.

26
Il problema della gerarchia di memoria è che non sempre quanto cercato si trova nel livello di memoria
desiderato. Infatti, quando il dato desiderato non è nella cache si parla di cache miss; tale dato dovrà quindi
essere prelevato dalla memoria principale, mettendo in stallo il processore e dunque sprecando cicli di
clock. Ancora, quando il dato desiderato non è nemmeno nella memoria fisica si parla di page fault; a
questo punto esso dovrà essere prelevato dalla memoria principale insieme all’intera pagina in cui si trova,
mettendo in stallo il processore per migliaia di cicli di clock. La cache miss ed il page fault devono essere
trasparenti al programma: tutto deve apparire come se non avessimo una gerarchia di memoria.

La gestione dei vari livelli di memoria è demandata all'unità di gestione della memoria, definita memory
management unit (MMU), la quale è una classe di componenti hardware che gestisce le richieste di
accesso alla memoria generate dalla CPU. La MMU può avere vari compiti tra cui la traduzione degli indirizzi
virtuali in indirizzi fisici, la protezione della memoria, il controllo della cache e l'arbitraggio del bus.

Avremo, in conclusione, 3 spazi di indirizzi, uno per ogni livello di memoria. Il processore, in particolare,
vede la memoria virtuale e genera indirizzi riferiti alla memoria virtuale; tali indirizzi vanno però mappati
sugli indirizzi fisici ed infine sugli indirizzi della cache, per permettere l’accesso del processore ad essi.

Mapping degli indirizzi


Mapping degli indirizzi virtuali in indirizzi fisici
Va fatta subito una precisazione per quanto riguarda i tre tipi di indirizzi per i tre differenti livelli di
memoria. La memoria cache va considerata come un array di blocchi di indirizzi copiati dalla memoria fisica.
Invece, la memoria virtuale va considerata come un array di pagine (blocchi di dimensione fissa di indirizzi)
copiati dalla memoria virtuale; quindi in questo caso vi è un mapping tra le pagine della memoria virtuale e
le pagine della memoria fisica.

Figura 10: Mapping delle pagine dallo spazio degli indirizzi virtuali allo spazio degli indirizzi fisici

Quando la CPU genera un indirizzo della memoria virtuale, esso sarà costituito dall’indirizzo della pagina
virtuale e da un offset, che indica la posizione dell’indirizzo stesso all’interno della pagina specificata.
Siccome la memoria fisica ospita copie delle pagine virtuali, allora, essa sarà organizzata allo stesso modo
della memoria virtuale. Dunque, anche l’indirizzo della memoria fisica sarà costituito dall’indirizzo della
pagina fisica (ovvero l’indirizzo della pagina virtuale tradotto) e dallo stesso offset dell’indirizzo virtuale.

27
Figura 11: Mapping di un indirizzo virtuale in un indirizzo fisico

Si mostra cruciale quindi progettare accuratamente la dimensione della singola pagina della memoria
virtuale. Si può pensare di scegliere la dimensione della pagina uguale alla dimensione della memoria fisica,
sicché la memoria fisica possa contenere una pagina alla volta ed allocare il processore su di essa; questa
però non è una buona idea se abbiamo più processi in esecuzione. Si può ben comprendere che si deve
fissare la dimensione della pagina in modo accurato. Rendere la pagina più grande aumenta la probabilità
di trovare nella memoria fisica gli indirizzi generati dal processore; difatti, per il principio di località, una
pagina più grande implica un tempo maggiore durante il quale troviamo ciò che cerchiamo in memoria
fisica. Rendere la pagina più piccola, invece, aumenta la probabilità che, dopo poche iterazioni, il
programma passerà ad un’altra pagina.

Allora, per sostituire le pagine, il modo più semplice sarebbe considerare la memoria fisica come un array
circolare, così da sostituirle in modo ordinato; questo però costituisce un problema in presenza di vari
processori, ciascuno allocato su una certa porzione di memoria fisica, in quanto ciascun processore non può
accedere alla porzione di memoria che non è ad esso riservata. Una soluzione migliore è l’impiego della
cosiddetta page table (tabella delle pagine), una tabella che risiede nella memoria fisica e che contiene le
traduzioni degli indirizzi virtuali in indirizzi fisici. Ogni programma ha la sua tabella delle pagine, che effettua
un mapping tra lo spazio degli indirizzi virtuali di quel programma ed il corrispondente spazio degli indirizzi
fisici. Inoltre, per indicare la locazione della page table in memoria, l’hardware include un registro, detto
page table register, che punta all’inizio della page table.

Siccome le tabelle delle pagine risiedono nella memoria principale, ogni programma per accedere in
memoria dovrebbe impiegare almeno il doppio del tempo, perché dovrebbe effettuare un accesso in
memoria per ottenere l’indirizzo fisico ed un secondo accesso per ottenere il dato. Per migliorare le
performance di accesso in memoria, allora, si può applicare il principio di località alla page table. Difatti, per
il principio di località, quando si utilizza la traduzione di una pagina virtuale, probabilmente essa verrà usata
di nuovo in un futuro prossimo. Per tale motivo, si può pensare di aggiungere al processore una speciale
cache, detta translation-lookaside buffer (TLB), che tenga traccia delle traduzioni utilizzate di recente.

28
Ovviamente, bisogna trovare un compromesso anche sulle dimensioni della page table e del TLB, in modo
da accorciare i tempi di accesso ad essi.

Figura 12: Schema di funzionamento della page table e del TLB

Mapping degli indirizzi fisici in indirizzi della cache


Abbiamo tre tipi di mapping degli indirizzi fisici in indirizzi della cache:
Mapping diretto (direct mapping)
Mapping completamente associativo (fully associative mapping)
Mapping set-associativo (set-associative mapping)

Nel mapping diretto, ogni locazione di memoria può essere mappata esattamente in una sola locazione
della cache. La posizione di un blocco di memoria in cache è data da:
(Indirizzo del blocco in memoria fisica) modulo (Numero dei blocchi in cache)
Questo schema è semplice ma comporta un problema: ci sarà un'unica locazione in cui potrà essere
posizionato un nuovo dato richiesto ma il dato che si trova in quella locazione potrebbe servirci ancora.

Nel mapping completamente associativo, invece, un blocco può essere posizionato in qualsiasi locazione
della cache. Per ritrovarlo, allora, bisogna scorrere tutte le entry della cache. Per rendere questa ricerca
pratica, la si effettua in parallelo con un comparatore associato ad ogni entry della cache. Tali comparatori
aumentano significativamente il costo dell’hardware e dunque rendono questo tipo di mapping praticabile
soltanto per una cache costituita da un numero esiguo di blocchi.

Il mapping set-associativo è una via di mezzo fra i primi due: esso permette un numero fissato di posizioni
(almeno due) in cui ogni blocco può essere copiato. La posizione del set contenente un dato blocco di
memoria è data da:
(Indirizzo del blocco in memoria fisica) modulo (Numero dei set in cache)
Una cache che utilizza un mapping set-associativo con n posizioni per ogni blocco è detta “ad n vie”. Una
cache set-associativa ad n vie consisterà, nello specifico, di un certo numero di set, ciascuno dei quali è
costituito da n blocchi. Ogni blocco in memoria mappa un unico set nella cache e può essere copiato in ogni
posizione di quel set.

29
Figura 13: Mapping diretto tra lo spazio degli indirizzi fisici e lo spazio in cache

Soluzioni architetturali e conclusioni


Il modello di Von Neumann è costituito da 3 componenti principali, comunicanti mediante un bus: CPU,
memoria e I/O. In esso, la CPU genera indirizzi riferiti allo spazio di indirizzi della memoria principale.

Introducendo una gerarchia di memoria, quindi, fondamentalmente andiamo a cambiare quello che è il
modello, aggiungendo i seguenti dispositivi: memoria cache, memoria virtuale (ospitata su un dispositivo
esterno) e MMU. Siccome essi sono dispositivi in più, vorremmo che essi siano removibili in modo da
tornare al modello base. In tal caso, lo spazio di indirizzi a cui la CPU farà riferimento sarà lo spazio di
indirizzi della memoria virtuale, che viene opportunamente mappato in memoria centrale e dunque in
cache. A causa di questa trasformazione, potrebbe accadere che il risultato della trasformazione stessa non
sia un indirizzo. Difatti, con questa modifica al modello, andiamo ad introdurre due funzioni:
f, il cui dominio è la CPU ed il cui codominio è la memoria centrale; tale funzione computa
l’indirizzo fisico
g, il cui dominio è la CPU ed il cui codominio è la memoria cache; tale funzione computa l’indirizzo
in cache
Siccome il dominio ed il codominio di tali funzioni hanno dimensioni differenti, l’algebra ci dice che le
funzioni stesse possono ritornare valori che non siano validi. Ora, dobbiamo distinguere i due casi.

Quando la funzione f non restituisce un indirizzo valido, abbiamo un page fault: esso genera un’eccezione,
ovvero una circostanza che segnala un problema nell’esecuzione di un’istruzione specifica. Un’eccezione va
risolta ad alto livello in quanto i dispositivi non possono risolvere autonomamente il problema. A tale
scopo, si effettuerà un restore della computazione, fornendo al processore l’indirizzo che ha generato il
page fault e lo stato della computazione stessa (salvato nel program status register), per permettergli di
tornare alla normale routine.

30
Quando la funzione g non restituisce un indirizzo valido, abbiamo una cache miss: essa non genera
un’eccezione, in quanto la MMU sa gestire tale circostanza, prendendo il blocco di indirizzi desiderati dalla
memoria centrale e copiandolo in cache. In tale caso, semplicemente l’esecuzione durerà di più in quanto si
perderanno dei cicli di clock per copiare gli indirizzi dalla memoria fisica alla cache. Per velocizzare tale
operazione, la MMU deve essere una macchina sequenziale dotata di clock, per accedere agli indirizzi in
modo sequenziale.

Detto ciò, si può fare osservazione: i problemi di basso livello sono problemi sintattici, che costano poco in
termini di spreco di risorse, invece i problemi di alto livello sono problemi semantici, la cui risoluzione
implica costi elevati.

Tecniche di caching
Vi sono due principali tecniche di caching, ovvero di aggiornamento della memoria centrale in seguito ad
una store:
Tecnica di write-through, che aggiorna nello stesso istante la cache e la memoria centrale, per
mantenere sempre i dati coerenti tra le due. È una tecnica estremamente sicura ma implica un calo
di performance, in quanto ogni volta che si esegue una store bisogna mettere in stallo la pipeline
per aggiornare la memoria centrale.
Tecnica di write-back, che aggiorna soltanto la cache al momento della scrittura e poi aggiorna la
memoria centrale al termine del programma. È una tecnica che migliora le performance ma che
rende, per un certo lasso di tempo, i dati della memoria centrale incoerenti rispetto ai dati reali
(salvati in cache). Può comportare problematiche se si verificano interruzioni del programma.

Meccanismi simili sussistono anche tra la memoria centrale e la memoria virtuale, la quale è lo spazio “di
riferimento” dei programmi e della CPU.

Tecniche di sostituzione delle pagine e dei blocchi


Per decidere la pagina nella memoria centrale che va sostituita con una nuova pagina della memoria
virtuale, si utilizza il cosiddetto algoritmo least recenty used (LRU), il quale essenzialmente sfrutta il
principio di località e sostituisce la pagina usata meno di recente rispetto alle altre. Siccome la dimensione
di una pagina è, difatti, abbastanza grande, essa permette l’osservazione degli indirizzi utilizzati dal
programma per un ampio lasso di tempo e consente una buona stima della località degli indirizzi. Quindi, si
possono usare ad esempio 2 o 3 bit per ogni pagina, come contatore dei colpi di clock trascorsi dall’ultima
volta che la pagina non è stata acceduta, ed in base ad essi sostituire la pagina con il contatore più alto.

Un algoritmo LRU può essere sia embedded nell’architettura sia demandato al sistema operativo. Il
problema di tali approcci è che sono molto onerosi perché causerebbero numerosi interrupt e dunque un
calo notevole delle prestazioni. A tale scopo, allora, si rende più smart il supporto alla memoria virtuale, nel
senso che l’interfaccia di I/O che controlla tale supporto è capace di accedere autonomamente alla
memoria virtuale per effettuare la sostituzione delle pagine. Tale interfaccia è definita direct memory
access (DMA). In pratica si notifica al DMA che si vuol prendere una pagina dalla memoria virtuale e la si
vuol copiare in un certo indirizzo in memoria centrale o viceversa. Durante tale intervallo di tempo, il
processo che ha richiesto il servizio resta in attesa che tale trasferimento avvenga, mentre la CPU effettua
altre operazioni.

31
L’architettura praticamente incarica il sistema operativo di gestire la gerarchia di memoria perché parte di
questa (la memoria virtuale) è ospitata su un dispositivo esterno, il cui accesso deve essere gestito appunto
dal sistema operativo, al quale perciò sono forniti dei registri appositi.

Invece, la maggior parte dei processori utilizza una random strategy per decidere il blocco nella cache che
va sostituito con un nuovo blocco della memoria centrale. La spiegazione di ciò è data dal fatto che in
genere abbiamo più processi in atto, ai quali è assegnata una piccola porzione della cache, dunque un
esiguo numero di indirizzi. Quindi, il tempo per osservare il comportamento di un programma e trarne
predizioni è troppo breve per effettuare predizioni accurate. I vantaggi di tale approccio sono il risparmio di
tempo e di memoria, lo svantaggio si nota ad un livello più alto ed è un calo della hit rate.

32
L’Esecuzione Fuori Ordine
Finora abbiamo ipotizzato di lavorare con pipeline lineari ma, in realtà, i processori moderni adottano
architetture più complesse, caratterizzate da più unità funzionali (UF) che operano in parallelo: si parla di
architetture scalari e superscalari, in quanto permettono di avviare ed eseguire più istruzioni in
contemporanea. Tali architetture presentano vari gradi di complessità ma possono raggiungere prestazioni
estremamente elevate.

Pipeline con unità funzionali multiciclo


Finora abbiamo ipotizzato che la fase di execute delle istruzioni duri un ciclo di clock: tale ipotesi ci vincola a
dimensionare il periodo di clock sulle operazioni che impiegano il tempo più lungo per essere eseguite (esse
sono le operazioni sui numeri in virgola mobile, dette floating point operations). Questo penalizza
fortemente le prestazioni del processore. Per migliorarle, si può adoperare più di una unità funzionale nella
fase di execute, introducendo un parallelismo. Si rimarca che l’esecuzione fuori ordine, ovvero la modifica
dell’ordine di esecuzione delle istruzioni, deve essere controllata in modo da rendere sempre coerente lo
stato della macchina con l’esecuzione sequenziale delle istruzioni.

La presenza di più unità funzionali, ciascuna con latenze differenti, implica la possibilità di un’esecuzione
fuori ordine, ovvero il completamento delle operazioni in un ordine differente da quello architetturale.
Difatti, date due istruzioni X e Y, con X che precede Y, se X impiega più cicli di clock di Y per essere eseguita,
l’ordine sequenziale delle istruzioni risulterà invertito, comportando una incoerenza nel programma.
Questa situazione vale anche in presenza di istruzioni di salto condizionato: la previsione sul salto potrebbe
essere errata, comportando così l’avvio di istruzioni non richieste. In particolare, chiamiamo “esecuzione
speculativa” il meccanismo che si riferisce alla previsione delle diramazioni e alla conseguente esecuzione
delle istruzioni prima che sia noto l’esito della diramazione stessa.

Figura 14: Pipeline con unità funzionali multiciclo

33
Rispetto alla classica pipeline lineare, riscontriamo le seguenti differenze:
Eliminazione dello stadio di memory, poiché tra le unità funzionali nella fase di execute ne compare
una dedicata alle operazioni di load/store in memoria. La presenza di questa unità è una soluzione
al problema della latenza della memoria.
Aggiunta di un issue register (unità di emissione) all’interno dello stadio di decode. Esso è
sostanzialmente un buffer nel quale sostano le istruzioni in attesa di essere inviate all’opportuna
unità funzionale.
Aggiunta delle seguenti unità funzionali (UF) nello stadio di execute:
o Unità logico-aritmetica principale in aritmetica intera
o Moltiplicatore in aritmetica intera e in virgola mobile
o Sommatore in virgola mobile
o Divisore in aritmetica intera e in virgola mobile
o Unità per la gestione delle diramazioni
o Unità per la gestione delle operazioni di load/store
Presenza di un unico bis, detto result bus (bus dei risultati) per i risultati di tutte le unità funzionali

In particolare, distinguiamo tre metodi principali per la gestione dell’esecuzione delle istruzioni in una
pipeline con unità funzionali multiciclo:
Metodo del completamento in ordine delle istruzioni, nel quale si forzano le istruzioni ad essere
completate seguendo l’ordine architetturale
Metodo del buffer di riordinamento, nel quale le istruzioni sono completate fuori ordine ma sono
forzate a modificare lo stato della macchina seguendo l’ordine architetturale
Metodo dell’history buffer, nel quale si permette alle istruzioni di aggiornare lo stato in qualsiasi
ordine, conservando però la possibilità di ripristinare uno stato coerente in presenza di conflitti

Gestione delle prenotazioni del bus dei risultati


Come abbiamo detto, le varie unità funzionali sono collegate al register file (RF) attraverso un solo bus dei
risultati: è necessario quindi un meccanismo di prenotazione del bus stesso.

Figura 15: Organizzazione di un processore con n unità funzionali multiciclo

34
Il protocollo di prenotazione si avvale del reservation shift register (RSR o registro a scorrimento dei
risultati) che presenta una struttura la quale tiene conto delle istruzioni in esecuzione, dei cicli di clock che
esse impiegano e del loro ordine architetturale.

Figura 16: Struttura del RSR

Nello specifico, i campi del RSR sono:


UF, identificatore dell’unità funzionale che ha eseguito l’istruzione
Rd, registro di destinazione su cui dovrà essere scritto il risultato dell’istruzione
V, un bit di validità impiegato per indicare se la posizione del registro RSR rappresenta o meno una
effettiva prenotazione
PC, valore del program counter relativo all’istruzione (questo campo è utile per il ripristino di uno
stato coerente in caso di interruzioni o di predizioni di diramazione errate)
Spieghiamo brevemente il funzionamento del RSR. Supponiamo che venga emessa l’istruzione A, che
impiega j cicli di clock. Tale istruzione viene inserita (prenotazione) nella posizione j-esima del RSR. Ad ogni
ciclo di clock, tutte le istruzioni presenti nel RSR si spostano verso l’alto di una posizione: questo vuol dire
che la prenotazione dell’istruzione A passa dalla posizione j alla posizione j−1. Quando una prenotazione
arriva in cima al RSR (posizione 1), vuol dire che al prossimo colpo di clock la corrispondente istruzione avrà
prodotto il suo output. Dunque, al prossimo colpo di clock, il risultato proveniente dall’unità funzionale
corrispondente verrà inviato sul bus dei risultati per la scrittura nel register file e la relativa prenotazione
verrò cancellata dal RSR. Se al momento dell’emissione dell’istruzione A, la posizione j-esima è occupata da
un’altra prenotazione, l’istruzione A attende all’interno dell’issue register per un ciclo di clock, dunque
riprovare a inoltrare la prenotazione della posizione j-esima.

35
Metodo del completamento in ordine delle istruzioni
Nel metodo del completamento in ordine si vanno a forzare le istruzioni affinché esse siano completate
secondo l’ordine architetturale. Distinguiamo due casi di completamento in ordine delle istruzioni:
Completamento in ordine rispetto ai registri
Completamento in ordine rispetto alla memoria
Entrambi i metodi proposti hanno lo stesso effetto: impedire che istruzioni che precedono un’istruzione di
memorizzazione vengano completate a memorizzazione effettuata. La differenza è che nel primo caso la
store attende nell’issue register il suo turno, nel secondo caso viene inserita in RSR con la possibilità di
avviare istruzioni successive.

Completamento in ordine rispetto ai registri


Nel completamento in ordine rispetto ai registri si fa in modo che nella prenotazione del bus dei risultati
nessuna istruzione “passi avanti” ad altre. A tal fine, l’istruzione che prenota la posizione j nel RSR occupa
anche tutte le posizioni libere da j−1 a 1, asserendo a 1 i bit di validità di quelle posizioni con il significato di
“posizioni riservate”. Questo implica che, se l’istruzione A necessita di m cicli di clock, con m ≤ j−1, essa
dovrà attendere nell’issue register finché la posizione j non verrà liberata.
Tale tecnica è molto semplice ma non sfrutta appieno il parallelismo delle unità funzionali, in quanto le
istruzioni più lente bloccano nell’issue register le istruzioni più veloci che le seguono.

Completamento in ordine rispetto alla memoria


Il completamento in ordine rispetto alla memoria si può realizzare in due modi:
Il processore non emette alcun comando di memorizzazione (store) prima che le istruzioni
precedentemente emesse siano state completate in maniera corretta. Questo è possibile
permettendo all’issue register di emettere istruzioni di memorizzazione solo quando il RSR è vuoto.
Si considera la memoria come una particolare unità funzionale. Analogamente a ogni istruzione,
l’istruzione di memorizzazione occupa una posizione in RSR in modo tale che questa raggiunga la
cima quando tutte le istruzioni emesse in precedenza sono state completate. Raggiunta la cima,
l’unità load/store viene comandata all’esecuzione della memorizzazione richiesta.

Metodo del buffer di riordinamento


Il metodo del buffer di riordinamento permette alle istruzioni di essere eseguite fuori ordine, consentendo
l’esecuzione speculativa delle istruzioni. Esso si fonda sull’aggiunta di un buffer, definito appunto reorder
buffer (ROB o buffer di riordinamento), nel quale le istruzioni completate dalle unità funzionali vengono
riordinate in modo da mantenere la coerenza tra lo stato della macchina ed il modello sequenziale di
esecuzione: le istruzioni sono avviate alle unità funzionali solo se esse non dipendono dai risultati delle
istruzioni presenti nel ROB.

Il ROB è gestito come una coda circolare con un puntatore di testa P t e un puntatore di coda Pc. Sono
considerate valide le entrate che si trovano nelle posizioni comprese tra quella puntata da Pt e quella
puntata da Pc. In presenza di ROB, il RSR non presenta il campo PC, bensì il campo pROB (valore del
corrispondente Pc al momento dell’entrata) che serve a mantenere la corrispondenza tra l’istruzione nel
RSR e la posizione nel ROB.

36
Figura 17: Organizzazione di una pipeline con il metodo del ROB

Nello specifico, i campi del ROB sono:


Rd (spostato dal RSR al ROB), campo contenente l’identificatore del registro di destinazione del
risultato dell’istruzione
Risultato, ovvero il risultato dell’istruzione
C, un bit di completamento indicante se l’istruzione è stata eseguita o meno
PC, valore del program counter relativo all’istruzione
All’emissione di un’istruzione, le informazioni richieste dal ROB vengono inserite nella posizione puntata da
Pc, con bit C posto a 0. Pc viene incrementato per puntare alla posizione successiva. Il funzionamento del
RSR, invece, risulta invariato, con l’unico accorgimento che al posto di PC ci sarà pROB, indicante il valore di
Pc al momento dell’entrata. Al completamento di un’istruzione, ovvero quando essa emerge dal RSR, il suo
risultato viene scritto nel corrispondente campo Risultato del ROB (anziché essere scritto sul bus dei
risultati) e viene asserito ad 1 il suo bit C di completamento. Infine, solo quando un elemento è raggiunto
dal puntatore Pt, il ROB attende che il corrispondente bit di completamento valga 1, dunque ne scrive il
risultato nel registro di destinazione Rd. Lo stato dei registri si modifica, perciò, in maniera coerente col
modello sequenziale e il puntatore Pt viene incrementato finché non raggiunge Pc.

Figura 18: Organizzazione del ROB e del RSR in sua presenza

37
Questo metodo forza lo stato ad essere aggiornato in ordine sequenziale, pur permettendo alle istruzioni di
terminare la loro esecuzione fuori ordine. Quindi, le istruzioni che arrivano all’issue register e che
dipendono da risultati che sono ancora nel ROB restano in attesa finché i dati vengano scritti nel banco RF.
Ciò comporta inevitabilmente a una riduzione delle prestazioni della CPU.

Un metodo per ridurre tale penalità consiste nel disporre dei percorsi di bypass che connettono gli elementi
presenti nel ROB con le uscite del RF. In questo modo le istruzioni possono usare come operandi sia i dati
contenuti nel RF sia quelli che ancora si trovano nel ROB. Quindi, questi percorsi rendono disponibili i dati
appena calcolati alle istruzioni in attesa di emissione, riducendo al minimo il tempo di permanenza delle
istruzioni stesse nell’issue register.

Metodo dell’history buffer


Per migliorare le prestazioni del metodo del ROB, la gestione dell’esecuzione fuori ordine può avvenire
anche mediante l’uso di un history buffer (HB), ovvero di un registro che tenga traccia dell’evoluzione dello
stato della macchina durante l’esecuzione del programma. L’idea di base è quella di permettere alle
istruzioni di completare l’esecuzione e di modificare il register file in qualsiasi ordine, conservando però
abbastanza informazioni sugli stati passati in modo da poter ripristinare, in caso di necessità, lo stato
appropriato.

Figura 19: Organizzazione della pipeline con un history buffer

L’HB è gestito come una coda circolare con un puntatore di testa Pt e un puntatore di coda Pc, come il ROB.
Sono considerate valide le entrate che si trovano nelle posizioni comprese tra quella puntata da Pt e quella
puntata da Pc. In presenza di HB, il RSR è gestito esattamente come nel caso del ROB: in particolare, il RSR
non presenterà il campo PC, bensì il campo pHB (valore del corrispondente Pc al momento dell’entrata) che
serve a mantenere la corrispondenza tra l’istruzione nel RSR e la posizione nell’HB.

38
Figura 20: Organizzazione di HB e di RSR in sua presenza

Nello specifico, i campi dell’HB sono:


Rd, identificatore del registro di destinazione del risultato dell’istruzione
Vecchio valore, ovvero il vecchio valore contenuto nel suddetto registro di destinazione, copiato in
HB dal RF al momento dell’invio dell’istruzione all’opportuna unità funzionale
C, un bit di completamento che, come nel ROB, serve a indicare se l’istruzione è già stata eseguita
PC, il valore del program counter corrispondente all’istruzione
Quando un’istruzione viene inviata dall’issue register a un’unità funzionale, viene effettuata la sua
prenotazione del bus dei risultati tramite RSR e viene memorizzato in HB un nuovo elemento ad essa
corrispondente. L’emissione dell’istruzione richiede che siano risolte le dipendenze, quindi le istruzioni che
hanno la necessità di leggere registri non ancora scritti sono messe in attesa. Quando un’istruzione viene
completata, il suo risultato viene immediatamente scritto nel relativo registro di destinazione; a tal fine
viene aggiunto anche a RSR il campo Rd. In particolare, quando l’istruzione arriva in testa all’HB col bit di
completamento pari a 1 e non è sorta nessuna necessità di ripristinare lo stato coerente, il corrispondente
elemento in HB non è più necessario e viene eliminato (aggiornando il P t).

I motivi per i quali si può voler ripristinare lo stato corrente sono legati alle interruzioni e alle previsioni di
salto errate. In presenza di interruzione, l’emissione di nuove istruzioni da parte dell’issue register viene
immediatamente bloccata. Sulla base del criterio di risoluzione dell’interruzione, viene identificata
l’istruzione in pipeline che discrimina quelle che vengono portate a completamento da quelle che vengono
annullate. Le istruzioni che devono essere completate scrivono nel RF e vengono estratte dall’HB. Per le
istruzioni da annullare, che fossero eventualmente completate e avessero scritto il loro registro di
destinazione, viene ripristinato il vecchio valore contenuto nell’HB. Servita l’interruzione il programma
riparte dalla prima istruzione che non è stata fatta procedere.

39
Gestione dei conflitti di controllo
Quando la predizione di una certa diramazione risulta errata, alcune istruzioni provenienti dal percorso
errato del flusso di istruzioni si trovano nella pipeline. Nel caso di pipeline lineare basta svuotare la pipeline
e ricominciare il prelievo dell’istruzione di destinazione reale della diramazione, poiché le istruzioni
provenienti dal percorso errato non hanno avuto il tempo di modificare lo stato della macchina. Nel caso di
esecuzioni fuori ordine, invece, il problema è più complesso, in quanto istruzioni provenienti dal percorso
errato possono essere state completate prima della soluzione della diramazione. In tal caso, lo stato della
macchina è modificato e non è sufficiente svuotare la pipeline e prelevare l’istruzione di destinazione.
Tale problematica è causata, oltre che dalle speculazioni errate, anche da tutti gli eventi che impongono un
“congelamento” del sistema, come le interruzioni.

Dunque, affinché il programma proceda correttamente anche nel caso di predizioni errate dei salti o di
interruzioni, la gestione dell’esecuzione fuori ordine deve soddisfare le seguenti condizioni:
Le istruzioni che sequenzialmente precedono l’istruzione di salto devono completare l’esecuzione e
modificare lo stato della macchina
Le istruzioni che sequenzialmente seguono l’istruzione di salto (provenienti dal percorso errato)
devono essere eliminate e non devono aver modificato lo stato della macchina
Nel caso del metodo del completamento in ordine delle istruzioni, le istruzioni per definizione sono forzate
a essere eseguite rispettando l’ordine architetturale, dunque l’esecuzione speculativa è di fatto impedita e
non vi è alcuna necessità di modifiche. Infatti, quando un’istruzione di salto è presente nel RSR, tutte le
istruzioni prelevate speculativamente occupano sole le posizioni inferiori: basterà svuotare il RSR e
prelevare l’istruzione del percorso corretto.

Nel caso del metodo del buffer di riordinamento, per ripristinare lo stato coerente nel caso di predizione
errata del salto, si introduce nel ROB un ulteriore campo, contenente il bit Epr, che indica se l’istruzione è
una diramazione che ha provocato un’errata predizione. Quando un’istruzione di salto viene completata,
assieme ai risultati viene inserita nel ROB anche la condizione di errata predizione. Se Epr indica che vi è
stata una predizione errata, una volta che l’elemento raggiunge la testa del buffer, si svuota la pipeline
negando l’accesso ai registri e a qualsiasi istruzione di scrittura e si preleva l’istruzione proveniente dal
percorso corretto.

Nel caso del metodo dell’history buffer, come nel metodo del ROB, si ricorre all’aggiunta del bit Epr di
errata predizione come campo dell’history buffer. Quando un’istruzione arriva in testa al buffer con il bit di
completamento pari a 1, viene controllato il relativo bit di errata predizione. Se esso indica che l’istruzione
in questione non ha causato nessuna speculazione errata, il corrispondente elemento in HB non è più
necessario e viene quindi eliminato. Invece, se Epr vale 1 si devono compiere, per ripristinare lo stato
coerente, i seguenti passi (roll-back):
Viene bloccato l’HB e viene bloccata l’emissione di nuove istruzioni da parte dell’issue register
Si attende che le istruzioni che precedono la diramazioni vengano completate; si attende cioè che la
diramazione si porti in testa al buffer
Partendo dalla coda verso la testa dell’HB vengono cancellati gli elementi e, contemporaneamente, i
loro vecchi valori vengono riscritti nei registri di destinazione, fino ad arrivare alla prima istruzione sul
percorso sbagliato
L’esecuzione del programma riprende prelevando l’istruzione proveniente dal percorso corretto

40
41
ANALISI PRESTAZIONALE DI UN
SISTEMA DI ELABORAZIONE
Introduzione
I sistemi di elaborazione sono al giorno d’oggi onnipresenti in ogni contesto sociale e lavorativo, per fornire
i più disparati servizi. È importante, quindi, che essi rispecchino dei criteri che definiscano la qualità del
servizio (quality of service o QoS). Gli indici della quality of service sono i seguenti:
1. Tempo di risposta o response time
2. Produttività o throughput
3. Disponibilità o availability
4. Affidabilità o reliability
5. Sicurezza o security
6. Scalabilità o scalability
7. Estensibilità o extensibility

A tale scopo, abbiamo bisogno di un modello matematico in grado di:


Indicare le migliori scelte progettuali affinché un sistema di elaborazione rispetti i requisiti
Valutare le prestazioni delle scelte progettuali di un sistema di elaborazione prima di
implementarlo
Individuare le criticità di un sistema di elaborazione e risolverle
Tale modello, come vedremo, si baserà sull’approccio probabilistico della teoria delle code.

Indici della Quality of Service


Adesso, esploriamo nel dettaglio gli indici della qualità del servizio.

Tempo di risposta o response time


Il tempo di risposta è il tempo che intercorre tra il momento in cui un utente richiede un servizio dal
sistema ed il momento in cui il sistema eroga il servizio a tale utente. Esso è il primo indice di QoS a cui è
interessato l’utente del servizio, è generalmente misurato in secondi ed è il risultato della somma di due
tempi principali, a loro volta influenzati da vari fattori:
Service time (tempo di servizio), che caratterizza le operazioni effettuate dal sistema.
Esso è un tempo legato a parametri oggettivi del sistema stesso ed è la somma di tre tempi:
o Tempo di elaborazione (processing time), legato all’esecuzione delle operazioni per erogare
il servizio
o Tempo di gestione di I/O (I/O time), necessario per accedere alle periferiche ed in
particolare ai dispositivi di memoria
o Latenza di trasmissione (network time), impiegato per la trasmissione (dall’utente al
sistema e viceversa) dei dati utili all’erogazione del servizio
Waiting time (tempo di attesa), che è legato al numero dei processi in coda che devono essere
eseguiti e smaltiti prima di servire la richiesta in questione. A differenza del tempo di servizio, il
tempo di attesa non è ben quantificabile, in quanto dipende da fattori non oggettivi, come il
numero di code (di tipo hardware e software) per le quali la richiesta dovrà passare o ancora il
numero di processi in coda prima della richiesta in questione. Per tale motivo, il tempo di attesa
può crescere in modo esponenziale in funzione del carico di informazioni da processare ed in tal
caso diviene il tempo dominante nel tempo di risposta.

43
Facciamo un esempio: supponiamo che un client, mediante il suo browser, richieda una pagina web ad un
server. Vogliamo stimare il tempo di risposta della richiesta della pagina web.
Tale tempo è costituito dalla somma di tre tempi principali:
Browser time, che include i tempi di processing e di I/O lato utente. Tali tempi sono necessari a
formulare ed inviare la richiesta HTTP e poi a ricevere, elaborare e visualizzare la pagina web.
Network time, che è legato alle comunicazioni fra il browser ed il suo ISP (Internet Service
Provider), fra il server ed il suo ISP ed infine fra i due ISP in Internet.
Server time, che include tutti i tempi di processing e di I/O lato server. Tali tempi sono legati alla
fornitura del servizio, ad esempio all’accesso a vari database.

Tali tempi includono del tempo di attesa, speso per aspettare l’utilizzo di differenti risorse (processori,
dischi e reti): esso è detto tempo di congestione. Il tempo di congestione dipende in modo proporzionale
dal numero di richieste che devono essere processate dal sistema in un determinato momento.

Throughput o produttività
Il throughput è il numero di richieste smaltibili dal sistema nell’unità di tempo ed è quindi un indice di
produttività del sistema stesso. Esso è il primo indice di QoS a cui è interessato il fornitore del servizio, si
misura in operazioni per unità di tempo e dunque la natura delle operazioni e l’unità di tempo dipendono
dallo specifico sistema, come vediamo nella seguente tabella.

Sistema Metrica di throughput


Sistema OLTP Transazioni al secondo (tps)
Transazioni al minuto (tpm-C)
Sito web Richieste HTTP al secondo
Pagine visualizzate al secondo
Router Pacchetti al secondo (PPS)
MB trasferiti al secondo
CPU Milioni di istruzioni al secondo (MIPS)
Operazioni in virgola mobile al secondo (FLOPS)
Server di posta E-mail inviate al secondo

Quando consideriamo una metrica di throughput, dobbiamo assicurarci che le operazioni da misurare siano
ben definite. Per far sì che il throughput sia significativo, allora, bisogna caratterizzare il tipo di transazioni.
In alcuni casi, questa caratterizzazione è definita da un benchmark (insieme di test software volti a fornire
una misura delle prestazioni) effettuato da una determinata azienda.

Ad esempio, in un sistema OLPT (Online Transaction Processing, insieme di tecniche software utilizzate per
la gestione di applicazioni orientate alle transazioni, quali il bancomat o i siti di e-commerce) il throughput
si può misurare in transazioni per secondo (tps). Le transazioni, però, possono variare significativamente a
seconda del tipo di operazioni e della quantità di risorse da elaborare. A tale scopo, l’organizzazione TPC
(Transaction Processing Council) definisce un benchmark per i sistemi OLTP, chiamato TPC-C, che specifica
un insieme di transazioni tipiche per questo tipo di sistemi. La metrica di throughput definita da tale
benchmark, allora, misura il numero di ordini processabili in un minuto ed è espressa in tpm-C.

44
Come possiamo vedere dalla curva superiore nella figura seguente, generalmente il throughput in funzione
del carico ha un andamento lineare, che tende ad assestarsi quando il sistema raggiunge la saturazione.
Ciò avviene quando una delle risorse del sistema raggiunge il 100% del suo utilizzo.
In alcuni casi, però, può capitare che l’andamento del throughput sia descritto dalla curva inferiore
presentata nella figura, ovvero può accadere che, all’aumentare del carico, il throughput possa calare
drasticamente: tale fenomeno è definito thrashing.

Figura 21: Throughput in funzione del carico

Ad esempio, il thrashing accade quando un computer con una memoria centrale insufficiente spende una
significativa quantità di cicli di clock per gestire i page fault anziché il carico di lavoro. Questo può avvenire
perché, in corrispondenza di carichi elevati, vi sono troppi processi in competizione sulla memoria: siccome
ogni processo ha meno memoria per sé, la frequenza dei page fault aumenta ed il throughput decresce.
Il sistema operativo, allora, inizia ad essere continuamente occupato a gestire operazioni di overhead (per
rimediare ai page fault) e ciò implica uno spreco di cicli di clock. Questo comporta un accumulo di
operazioni arretrare e può instaurare un circolo vizioso in cui il carico aumenta ed il throughput cala.

Disponibilità o availability
La disponibilità è il tempo medio in cui il sistema è disponibile ad erogare servizi agli utenti.
Vi sono due ragioni principali per cui un sistema può non essere disponibile:
Guasti (failures), interessano i componenti del sistema e impediscono agli utenti la fruizione del
servizio. La gestione dei guasti (failure handling) deve avvenire rapidamente, in tre fasi:
o Failure detection, possibile con un sistema di monitoraggio che individui e preveda i guasti
o Failure diagnosis, identificazione delle cause del guasto
o Failure recovery, possibile grazie ad una squadra di pronto intervento che sostituisca il
componente guasto
Eccessi di carico (overloads), avvengono quando i componenti del sistema funzionano
correttamente ma il sistema non ha abbastanza risorse per gestire tutte le richieste in ingresso.
In tal caso, in genere il sistema rifiuta le richieste, con un meccanismo definito admission control:
esso permette di controllare e limitare il numero di richieste che un sistema deve servire
contemporaneamente, in modo da garantire tempi di risposta accettabili ed un buon throughput.

45
Affidabilità o reliability
L’affidabilità è la probabilità che il sistema funzioni in maniera corretta e continua in un determinato
periodo di tempo. L’affidabilità e la disponibilità sono concetti strettamente correlati: quando aumenta il
tempo durante il quale il sistema è affidabile, l’affidabilità tende alla disponibilità.

Sicurezza o security
La sicurezza è la proprietà che garantisce che i dati di un sistema vengano utilizzati nella maniera corretta e
solo dalle persone autorizzate ad accedere ad essi. Quindi, la sicurezza è la combinazione di tre proprietà:
Confidentiality, la quale garantisce che solo le persone autorizzate hanno il permesso di accedere a
dati sensibili
Data integrity, che assicura che i dati non possono essere modificati in modo non autorizzato
Non-repudiation, la quale impedisce ai mittenti di negare di aver inviato un messaggio

Queste proprietà possono essere garantite attraverso meccanismi di autenticazione, i quali assicurano
comunicazioni affidabili da ambo le parti. Nello specifico, tali meccanismi si basano su una o più tecniche
crittografiche, le quali sono molto onerose dal punto di vista computazionale.

Infine, va detto che la sicurezza si differenzia dalla safety, la proprietà che garantisce che il sistema svolga
correttamente il proprio compito.

Scalabilità o scalability
La scalabilità è la proprietà per cui le prestazioni del sistema non degradano significativamente
all’aumentare degli utenti o del carico di lavoro.

Estensibilità o extensibility
L’estensibilità è la possibilità del sistema di evolvere facilmente per far fronte a nuove funzionalità e
specifiche. Difatti, capita spesso che vengano richieste nuove funzionalità ad un sistema già uscito sul
mercato.

46
Il ciclo di vita di un sistema IT
In questa sezione presenteremo le fasi del ciclo di vita di un qualsiasi sistema IT:
1. Analisi e specifica dei requisiti (requirement analysis and specification)
2. Progettazione (system design)
3. Implementazione (system development)
4. Collaudo (testing)
5. Rilascio (system deployment)
6. Messa in opera (system operation)
7. Manutenzione (system evolution)

Analisi e specifica dei requisiti


Durante la fase di analisi e specifica dei requisiti, i progettisti incontrano i clienti per acquisire informazioni
sul sistema da produrre. Il risultato di questa fase è un documento in cui si specificano i requisiti che il
sistema deve avere ed è costituito da due parti:
Specifiche funzionali, che indicano cosa il sistema deve fare.
Descrivono un insieme di funzioni che il sistema deve fornire, con i corrispondenti input ed output,
e solitamente includono anche informazioni riguardanti l’ambiente in cui verrà inserito il sistema e
le tecnologie che devono essere utilizzare per progettarlo ed implementarlo.
Specifiche non funzionali, che indicano come il sistema deve svolgere le sue funzioni
(sostanzialmente sono gli indici di quality of service richiesti).
Descrivono in maniera qualitativa e quantitativa il carico di lavoro.

Progettazione
Durante la fase di progettazione, i progettisti pensano a come realizzare il sistema affinché rispetti le
specifiche descritte nella fase precedente. Il risultato di questa fase è un progetto che delinei ad alto livello
l’architettura del sistema e la suddivida in sottosistemi e blocchi, spechificando le interfacce fra questi
ultimi. Il progetto include la descrizione di strutture dati, file, database, algoritmi da implementare o
riusare, comprendendo porzioni di pseudo-codice che esprima il funzionamento dei componenti principali.
Va detto che il riuso di soluzioni software e di componenti è largamente impiegato nell’ingegneria del
software, perché permette di accorciare i tempi di progettazione ed implementazione del sistema, ma può
comportare dei rischi in termini di performance: Infatti, una soluzione performante in un determinato
contesto può risultare scadente se viene applicata ad un differente contesto.

Più in generale, bisogna rimarcare che la fase di progettazione è una fase delicata, poiché le decisioni prese
in questa fase hanno un forte impatto sulle performance del sistema e sul prosieguo del progetto, in
termini di costi e di tempi.
Per concludere, può capitare che l’analisi delle performance in questa fase venga trascurata a causa della
mancanza di elementi concreti di valutazione. Questo è un errore, pertanto nel prosieguo introdurremo dei
metodi per modellare un sistema, in modo da prevederne le prestazioni e le criticità.

Implementazione
Durante la fase di implementazione, i vari componenti progettati precedentemente vengono implementati
e poi integrati. Alcuni dei componenti possono essere realizzati ex novo, altri possono essere riadattati a
partire da componenti esistenti, altri ancora possono essere semplicemente riusati da precedenti
implementazioni senza alcuna modifica. I componenti vengono poi interconnessi per formare il sistema.

47
Si noti che, così come esistono vari modi di progettare un sistema, esistono anche vari modi di
implementare alcune decisioni, lasciate in sospeso dalla fase precedente. Tali scelte devono essere prese in
modo da migliorare le performance e facilitare le fasi successive.

Collaudo
Generalmente la fase di collaudo avviene in modo concorrente allo sviluppo del sistema. Difatti, appena i
componenti vengono sviluppati, vengono testati in modo isolato (unit testing), poi vengono aggregati in
sottosistemi, che a loro volta vengono testati, e così via fino al test finale dell’intero sistema.
Il collaudo è finalizzato a verificare che il sistema rispetti le specifiche. Spesso, però, interessa in particolar
modo le specifiche funzionali e trascura quelle non funzionali: questo è un errore, in quanto va a tralasciare
il test delle performance del sistema. Pertanto, bisogna effettuare anche un collaudo delle performance del
sistema, solitamente realizzato mediante load testing, grazie a degli script che simulano transazioni tipiche.

Ovviamente, è impossibile anticipare e simulare qualsiasi tipo di configurazione in fase di collaudo, a causa
dei limiti imposti dal tempo e dal budget. Pertanto, può capitare che un sistema venga rilasciato senza
essere stato completamente testato per garantirne le specifiche. Questo può essere un grave errore,
pertanto, come già detto, nel prosieguo introdurremo dei metodi per modellare un sistema, in modo da
prevederne le prestazioni e le criticità ed includere le performance nel progetto di un sistema.

Rilascio
Dopo le fasi di sviluppo e test del sistema, generalmente in ambiente controllato, il sistema viene rilasciato
per essere utilizzato. Durante il rilascio, molti parametri di configurazione devono essere impostati per
garantirne una performance ottimale. Per questo motivo, nel prosieguo verranno descritti i modelli utili a
predire le performance di un sistema nel contesto di differenti scenari e di differenti parametri.

Messa in opera
Un sistema che è stato rilasciato deve essere costantemente monitorato per controllare che esso rispetti le
specifiche. In particolare, vi sono tre aspetti che dovrebbero essere monitorati:
Il workload. Esso va controllato sotto due aspetti:
o Dal punto di vista legale, per assicurarsi che il sistema rispetti le specifiche
o Dal punto di vista della sicurezza, perché dei picchi di carico possono indicare attacchi
fraudolenti, come il DoS (denial of service)
Le metriche di performance esterne (external performance metrics), che misurano il gradimento
degli utenti rispetto al sistema, come il tempo di risposta e la disponibilità del sistema
Le metriche di performance interne (internal performance metrics), che misurano le prestazioni del
sistema rispetto a chi lo ha commissionato, come il throughput, l’utilizzo delle risorse e la
risoluzione di situazioni di congestione
Durante la messa in opera di un sistema, può essere necessario modificare vari parametri di configurazione
in modo da adattare il sistema alla natura del carico, affinché esso rispetti costantemente gli indici di QoS.

Manutenzione
La maggior parte dei sistemi IT devono evolversi dopo che essi sono stati messi in opera per un certo
periodo di tempo, a causa di differenti fattori, quali cambiamenti di ambiente e contesto, nuovi requisiti
dell’utente o ancora nuove norme e leggi. È importante, allora, che il sistema sia stato progettato per
supportare i vecchi ed i nuovi workload nel rispetto degli indici di QoS. A tale scopo, vedremo nel seguito
alcuni modelli predittivi in grado di verificare tale proprietà.

48
Modellazione di un Sistema
Come abbiamo detto, la valutazione delle performance di un sistema deve essere effettuata in fase di
progettazione del sistema e non al termine del suo sviluppo, per evidenti motivi legati ai costi e ai tempi.
Pertanto, il presente capitolo descriverà il modo con cui si può modellare un sistema prima di svilupparlo,
affinché si possano valutare le sue performance in fase di progetto.

In tale contesto, modellare un sistema presuppone che esso sia visto come un insieme di risorse (quali
processori, dischi, moduli, ecc.) che lavorano in modo concorrente e si scambiano dati attraverso protocolli.
Spesso, ci sono varie richieste che vogliono accedere alla stessa risorsa nello stesso momento, ma le risorse
hanno una capacità limitata, pertanto causeranno la formazione di code di richieste. Allora, il modello che
andremo ad utilizzare è basato sulla teoria delle code e vede un sistema come una collezione di code
interconnesse, detta rete di code.

Tipi di modellazione
Un modello è un’astrazione, una vista ad alto livello di un sistema reale. Il livello di dettaglio di un modello e
gli aspetti del sistema che il modello tiene in considerazione dipendono dallo scopo del modello stesso.
In generale, però, un modello non deve essere reso più complicato del necessario per raggiungere il suo
scopo, ovvero non deve contenere dettagli superflui rispetto alle reali esigenze per cui è stato pensato.

Il modello perfetto di un sistema è il sistema stesso o un suo duplicato: riprodurlo è generalmente


impossibile o estremamente costoso. All’estremo opposto, il modello più impreciso ed infedele di un
sistema è un modello intuitivo, basato sull’esperienza, che perciò è semplice e gratuito da riprodurre.
La via di mezzo è riprodurre un modello accurato di un sistema, con un metodo scientifico, rigoroso ma non
estremamente costoso ed è ciò che vedremo nel seguito.

Un modello scientifico può essere di due tipi:


Modello di simulazione (simulation model), che si basa su un’applicazione che finge di essere il
sistema, ovvero che prende degli input e restituisce degli output come farebbe il sistema.
Generalmente un’applicazione del genere viene provata con dei workload standard, ovvero dei
benchmark che simulano il comportamento tipico del sistema, per riscontrare le differenti
dinamiche del sistema in base al carico applicativo a cui è sottoposto. In alternativa ai benchmark,
si può simulare il workload anche attraverso processi probabilistici che generano variabili aleatorie.
I modelli di simulazione sono sufficientemente precisi e possono essere sviluppati con differenti
livelli di dettaglio a seconda delle esigenze, ma richiedono spesso costi e tempi eccessivi.
Modello analitico (analytics model), che è un insieme di equazioni ed algoritmi in grado di restituire
risultati di performance in funzione di un set di parametri di workload in input. Generalmente un
modello analitico è più astratto ed efficiente rispetto ad un modello di simulazione, è più semplice
ed economico da realizzare ma anche meno dettagliato e preciso. Dobbiamo però rimarcare che
non sempre è possibile modellare matematicamente un sistema e che, se invece è possibile, la
quantità di dettagli del sistema influenza la sua complessità matematica.

49
In definitiva, un modello analitico è meno costoso da realizzare e tende ad essere computazionalmente più
efficiente; un modello di simulazione invece è più dettagliato, accurato ma più costoso. Per i nostri scopi,
utilizzeremo un modello analitico approssimato, che favorisca la trattabilità analitica e la semplicità
realizzativa, a patto di avere una stima del suo livello di imprecisione, che generalmente non deve superare
il 10% rispetto al reale sistema.

Queueing Network (QN)


Come abbiamo detto, modellare un sistema presuppone che esso sia visto come un insieme di risorse che
lavorano in modo concorrente: un esempio concreto di ciò è il processore. Nel modello che adottiamo, ogni
risorsa è rappresentata con un cerchio, eventualmente preceduta da una linea di attesa (rappresentata con
un rettangolo) in cui le richieste, dette customer, aspettano prima di essere processate. L’insieme della
risorsa e della sua linea di attesa è detta coda o queue. Vi sono anche casi in cui una sola linea d’attesa
preceda un insieme multiplo di risorse.

Figura 22: Schema di una coda con una risorsa (a) e con più risorse (b)

Un insieme di collegamenti e di risorse con le rispettive linee di attesa è detto rete di code o queueing
network (QN). Facciamo un esempio: un semplice database server con una sola CPU ed un solo disco può
essere rappresentato come una rete di code, come vediamo nella figura seguente. Le transazioni arrivano
al server per essere processate e devono attraversare varie volte la CPU ed il disco prima di essere
completate ed ovviamente devono essere messe in coda se sono precedute da altre transazioni.

Figura 23: Rete di code di un semplice database server

50
Tipi di classi
Supponiamo che il database server debba gestire differenti tipi di transazioni, che possono essere distinte
in tre gruppi in base ai loro requisiti prestazionali, come ad esempio nella seguente tabella. Si noti che
questa tabella rappresenta le richieste prestazionali che un cliente può desiderare da un sistema e che
dunque vengono raccolte in un contratto, definito service level agreement (SLA), il quale sostanzialmente
coincide con gli indici di QoS richiesti dal cliente.

Gruppo di Tempo medio di elaborazione Numero medio di


transazioni della CPU (in secondi) accessi al disco
Semplici 0,04 5,5
Medie 0,18 28,9
Complesse 1,2 85

In tal caso, non è appropriato caratterizzare le transazioni come un singolo gruppo, perché avremmo delle
valutazioni prestazionali errate. Perciò, quando si descrive una QN, bisogna specificare le classi di customer
che usano le risorse della QN, il workload di ogni classe e le richieste che ogni classe fa. Se un modello QN è
caratterizzato da più classi di customer, prende il nome di QN multiclasse (multiclass QN model).

Riassumendo, una QN multiclasse deve essere usata nei casi in cui si presentano:
Richieste di servizio eterogenee verso le risorse
Differenti tipi di workload
Differenti requisiti prestazionali

Se nella tabella sovrastante fosse specificata un’altra colonna detta “Percentuale sul workload totale”, che
descriva l’intensità del workload per ogni classe rispetto al workload totale, avremmo dei customer che
appartengono ad una classe aperta. Una classe aperta (open class) è caratterizzata come segue:
L’intensità del workload è specificata da una percentuale, detta arrival rate (tasso di arrivo), che
indica la frequenza con la quale le transazioni arrivano al sistema ed è indipendente dallo stato del
sistema
Il numero di customer del sistema non è limitato da un valore massimo
Il throughput è un parametro di input, dato dall’arrival rate.
Infatti, se il sistema è in equilibrio, il flusso di richieste in arrivo è uguale al flusso in uscita: ciò
implica che il throughput di una classe aperta è uguale all’arrival rate e dunque è noto.

Supponiamo che di notte il database server non sia disponibile per l’esecuzione di transazioni online. Esso è
invece usato per eseguire operazioni batch che producono report manageriali. In tal caso, la tabella iniziale
presenterà una colonna in più, detta “Numerosità”, che indica il numero di richieste nell’unità di tempo.
Una classe di questo tipo è detta chiusa. Una classe chiusa (closed class) ha le seguenti caratteristiche:
L’intensità del workload è specificata dalla numerosità dei customer, detta customer population,
che è il numero di richieste concorrenti in esecuzione
Il numero di customer del sistema è un parametro di input ed è limitato da un valore massimo
Il throughput è un parametro di output.
Il throughput di una classe chiusa si ottiene risolvendo il modello QN ed è una funzione della
numerosità della specifica classe.

51
Un modello QN in cui tutte le classi sono aperte è detto QN aperta (open QN), un modello QN in cui tutte le
classi sono chiuse è detto QN chiusa (closed QN). Un modello QN in cui le classi sono sia aperte sia chiuse è
detto QN mista (mixed QN).

Concludendo, consideriamo nuovamente il database server menzionato precedentemente. Assumiamo che


il cliente voglia che le transazioni online e le operazioni batch siano possibili contemporaneamente ad ogni
ora del giorno e specifichi le performance desiderate nella seguente tabella. Le prestazioni delle transazioni
online sono descritte in termini di un bound superiore sul tempo di risposta medio, mentre le prestazioni
delle operazioni batch sono descritte in termini di un bound inferiore sul throughput. Anche tale tabella
rappresenta un esempio di SLA.

Gruppo di Massimo tempo di risposta Minimo throughput


transazioni medio (in secondi)
Semplici 1,2 -
Medie 2,5 -
Complesse 8 -
Batch - 20 ogni ora

Nella seguente figura, vediamo la rappresentazione grafica del sistema suddetto: esso è una QN mista.

Figura 24: QN mista per un database server

Tipi di risorse
Supponiamo che il database server menzionato precedentemente sia usato per supportare un’applicazione
client/server. Le workstation dei client sono connesse al database server attraverso una LAN, i client
effettuano richieste al server indipendentemente gli uni dagli altri ed alternano ciclicamente fasi di
“thinking” (formulazione della richiesta) e di “waiting” (attesa della risposta del server). Quindi, possiamo
rappresentare il client come una risorsa senza linea di attesa, ovvero una risorsa delay.

Invece, la LAN che connette i vari client con il server è caratterizzata da una larghezza di banda che
decresce al crescere del numero di workstation dei client, a causa delle collisioni fra i pacchetti. Quindi, la
LAN può essere rappresentata come una risorsa in cui il carico influenza il service rate (frequenza di
servizio, ossia di processamento delle richieste). Una risorsa di questo tipo è una risorsa load-dependent ed
è graficamente rappresentata da un cerchio sbarrato con una freccia ed un rettangolo.

Infine, le risorse del database server, ovvero la CPU ed il disco, hanno un service rate costante, dunque
sono definite risorse load-independent e vengono rappresentate con un cerchio ed un rettangolo.

La seguente figura rappresenta la QN dello scenario appena descritto.

52
Figura 25: QN per un database server connesso ad un client attraverso una LAN

Riassumendo, esistono tre tipi di risorse in una QN:


Risorse load-independent (LI), che hanno un service rate costante, indipendente dal carico (dove il
carico è il numero di richieste in coda)
Risorse load-dependent (LD), che hanno un service rate dipendente dal carico.
Esse vengono usate per modellare una coda con m risorse. In tal caso, il service rate aumenta se il
numero di richieste aumenta da 1 a m, ma decresce se il numero di richieste supera m.
Risorse delay (D), che non presentano linee di attesa e che dunque servono immediatamente le
richieste

Meccanismo del blocking


Supponiamo che i gestori del database server presentato negli esempi precedenti vogliano garantire ai loro
clienti che il tempo di risposta non oltrepassi una certa soglia. Come sappiamo, il tempo di risposta dipende
dal tempo di servizio e dal tempo di attesa. In particolare, però, il tempo di servizio è costante nelle risorse
load independent, come nel presente caso, quindi bisogna agire sul tempo di attesa.

Per fare ciò, indipendentemente dal tasso di arrivo delle richieste, bisogna limitare il numero di transazioni
concorrenti: bisogna cioè implementare un meccanismo di admission control. Tale meccanismo pone un
limite W al numero di customer ammessi nel sistema: quando una richiesta trova già W customer nel
sistema, essa viene bloccata. Nello specifico, bloccare una richiesta può voler dire sia rifiutarla sia mandarla
in un’altra coda di attesa.

Figura 26: QN con blocking

53
In tal caso, il throughput non è necessariamente uguale al tasso di arrivo, ma dipende anche dalla
probabilità che una determinata richiesta venga accettata:

EFFECTIVE THROUGHPUT = ARRIVAL RATE x (1-Pr[REJECT])

Software contention
Supponiamo adesso che il database server suddetto sia multi-thread: avremo m thread, ciascuno dei quali
gestisce una transazione alla volta. Se tutti i thread sono occupati a servire le richieste, le transazioni in
entrata devono aspettare in coda affinché un thread si liberi. Perciò, è importante determinare m*, ovvero
il numero ottimale di thread per un certo tasso d’arrivo affinché sia rispettato il documento SLA.

Vi sono due fattori opposti che bisogna tenere in considerazione:


Il software contention, che è il tempo speso da una transazione nell’attesa di un thread disponibile
a processarla
Il physical contention, che è il tempo speso da una transazione nell’attesa di utilizzare le risorse

Con l’aumentare di m, il software contention diminuisce poiché vi saranno più thread a smaltire le richieste,
ma il physical contention aumenta poiché vi saranno più transazioni che si contenderanno le risorse.

Pertanto, il tempo di risposta può aumentare o diminuire in funzione di m, in base al fattore predominante
fra i due suddetti. Quando essi raggiungono l’equilibrio, troveremo un valore ottimale m*. La figura
seguente mostra due grafici del tempo di risposta in funzione di m, per due valori del tasso di arrivo medio,
λ1 e λ2, con λ1 < λ2. Possiamo notare che la curva per λ1 mostra il numero m* di thread che minimizza il
tempo di risposta medio per λ1. Va sottolineato che il numero di thread m* non è un valore assoluto, ma è
definito in funzione del workload, ovvero del tasso di arrivo delle richieste.

Figura 27: Grafici del tempo di risposta in funzione del numero di thread per due differenti tassi di arrivo

54
Nella figura successiva, possiamo infine vedere la rappresentazione dell’uso concorrente di un sistema, che
nell’esempio è l’insieme delle risorse della CPU e del disco.

Figura 28: Database server con software contention

SRP (Simultaneous Resource Possession)


Supponiamo adesso che nel database server vi sia una consistente attività di update. In tal caso, le
transazioni che devono effettuare l’update devono prima ottenere il lock del database, ovvero devono
ottenere il controllo totale del sistema. Ciò deve avvenire in modo da impedire ad altre transazioni di
accedere al sistema ed effettuare operazioni inconsistenti. Una volta acquisito il lock, una transazione potrà
allora utilizzare la CPU ed il disco in modo esclusivo. Una situazione in cui un customer in una QN è abilitato
a controllare una o più risorse allo stesso tempo è detta SRP (simultaneous resource possession).

La figura seguente mostra il sistema precedente con l’aggiunta dei lock, che effettivamente costituiscono
una risorsa. Il fatto che i lock controllino la CPU ed il disco è raffigurato con delle frecce tratteggiate che
vanno dai lock alla CPU e al disco.

Figura 29: QN con SRP

55
Queueing disciplines
Immaginiamo adesso che il database server debba gestire sia query sia update, ma che il documento SLA
esiga un tempo di risposta migliore per le query rispetto alle update. In questo caso, le query devono avere
un trattamento preferenziale, quale una priorità più alta nella CPU: ciò è possibile utilizzando una delle
politiche possibili in una rete di code, che è la coda a priorità.

Nello specifico, vi sono varie politiche di accodamento (queueing disciplines) per una QN. Esse sono:
FCFS (First Come First Served, riconducibile alla politica FIFO), in cui i customer sono serviti
nell’ordine di arrivo alla coda
Coda a priorità (priority queueing), che divide i customer in classi con differenti ordini di priorità.
Quando la risorsa si libera, essa serve il primo customer a priorità maggiore.
Si può vedere tale approccio come una coda in cui le differenti priorità sono marcate in modo
differente, o ancora come un insieme di code, ciascuna con un diverso grado di priorità.
Ci sono differenti approcci per quanto riguarda le code a priorità, quali:
o Priorità statica, in cui il grado di priorità di un customer non cambia nel tempo
o Priorità dinamica, in cui il grado di priorità di un customer cambia nel tempo
o Preemptive, in cui un customer a priorità maggiore può immediatamente prendere il
possesso di una risorsa che sta processando un customer a priorità minore
Preemptive resume, in cui il processamento del customer a priorità minore è
momentaneamente sospeso
Preemptive restart, in cui il processamento del customer a priorità minore viene
annullato
o Non-preemptive
Round-robin (RR), in cui ogni richiesta è servita per un breve periodo di tempo (detto quantum,
time-slice o slot) dopo il quale la risorsa è assegnata alla prossima richiesta, in modo circolare.
Questa politica corrisponde allo scheduling utilizzato dai sistemi operativi per assegnare i vari
processi alla CPU.
Processor sharing (PS), che corrisponde al limite teorico del round-robin quando il time-slice è
prossimo allo zero. Praticamente, quando si hanno n richieste in coda, si fa in modo che esse
vengono tutte servite simultaneamente, ma ognuna di esse vede la risorsa n volte più lenta.

56
Parametri prestazionali
Quando si tiene sotto osservazione un sistema per un dato intervallo di tempo, si parla di approccio
“operational analysis”. Esso ci consente di determinare molti parametri, noti e derivati, del sistema stesso.

Variabili operative (Parametri noti o misurati)


T: lunghezza del periodo di osservazione
K: numero di risorse nel sistema
Bi : tempo in cui la risorsa i è stata occupata durante il periodo di osservazione
Ai : numero di richieste ricevute dalla risorsa i durante l’intervallo di tempo T
Ci : numero di richieste processate dalla risorsa i durante l’intervallo di tempo T
A0 : numero complessivo di richieste ricevute dal sistema
C0 : numero complessivo di richieste completate dal sistema

Variabili derivate
Si = Bi/Ci : tempo di servizio medio della risorsa i
Ui = Bi/T : percentuale di utilizzo della risorsa (utilizzazione)
Xi = Ci/T : throughput della risorsa i
i = Ai/T : tasso di arrivo alla risorsa i
Vi = Ci/C0 : numero medio di visite alla risorsa i per richieste processate
X0 = C0/T : throughput complessivo del sistema 


In un modello multi-classe, tale approccio continua a valere ma con una estensione alle j = 1, 2, …, r classi.
Nello specifico, ogni parametro sarà caratterizzato da due pedici: uno che rappresenta la risorsa o il sistema
(come nel caso di una singola classe) ed uno che rappresenta la classe. Ad esempio, il parametro Ui,r
rappresenta l’utilizzazione della risorsa i da parte della classe r.

57