TRUCCHI
1. Gerarchia di memoria
1.1 Introduzione
La velocità dei circuiti elettronici dipende dalla velocità di commutazione delle loro porte logiche. Purtroppo, maggiore è la velocità di una porta, più
elevato sarà il suo prezzo.
La memoria centrale è composta da moltissime porte, almeno 4 per ogni bit che può contenere. La CPU è composta da molte meno porte. Questo comporta che la CPU può
essere costruita con porte più veloci di quelle della memoria centrale. Avere la RAM lenta, però, rallenta di conseguenza la CPU quando questa ha bisogno di recuperare stringhe
dalla memoria (molto spesso), vanificando la sua maggiore velocità.
Per evitare tutto ciò è stata introdotta la gerarchia di memoria, un accorgimento pratico senza nessuna funzione logica.
1.2 La memoria cache
Fra CPU e RAM viene interposta una
memoria costruita con la stessa tecnologia della CPU, detta cache memory o memoria nascosta. Essendo realizzata con porte più costose (le stesse della CPU), deve essere in
quantità molto minore rispetto alla RAM, per limitare i costi; ciononostante, essa può contenere centinaia di word.
La CPU non colloquia direttamente con la RAM, ma con la cache. Quando la control unit richiede un nuovo indirizzo alla cache, se questa ha la stringa relativa a quell'indirizzo, la manda alla
CPU direttamente, altrimenti carica dalla memoria centrale -passando attraverso il bus- non solo quella stringa,
ma tutto il blocco di stringhe limitrofe. Questo trasferimento da memoria centrale a cache avviene in modo parallelo:
in una sola volta vengono trasferite più stringhe. Tramite questo accorgimento, la gerarchia di memoria funziona molto bene e permette alla CPU di lavorare quasi sempre alla sua massima
velocità, colloquiando solo con la cache.
Si noti che nonostante cache e RAM comunichino tramite il bus di sistema, riescono ad effettuare trasferimenti in parallelo di più word contemporaneamente. Il bus, permette, infatti,
trasferimenti in parallelo. Quante word parallelamente riesca a far transitare la macchina è una scelta progettuale.
Perchè tutto funzioni correttamente, è necessario che la cache lavori con una logica diversa da quella della memoria centrale. La cache è, infatti, una memoria di tipo
associativo e non sequenziale. Le memorie di tipo associativo ricevono una maschera (= una parte) del contenuto che si cerca e restituiscono tutto il contenuto associato a quella maschera. In
particolare, nella cache ogni registro non contiene soltanto il dato vero e proprio, ma anche il suo indirizzo in memoria centrale (che è la maschera). Alla cache arriva una parte della
stringa cercata - l'indirizzo - ed essa restituisce tutto il dato - indirizzo + dato vero e proprio.
Ciò è necessario perchè permette di memorizzare dati in qualsiasi ordine e non sequenzialmente (dopo il dato di indirizzo 0000, nella RAM ci deve essere il dato di indirizzo
0001, nella cache ci può essere qualsiasi dato). Questo è indispensabile proprio per come funziona la cache.
Ultime osservazioni: questo meccanismo gerarchico risulta trasparente al programmatore assembly; programmi che fanno spesso salti molto ampi richiedono dati non contenuti nella cache e quindi
non sfruttano tale meccanismo, per questo alcuni costruttori forniscono istruzioni assembly di salto con un offset limitato; in questo corso non si tratteranno i criteri per la sostituzione dei
dati all'interno della cache.
1.3 Livelli di cache
Ci possono essere diversi livelli di
cache, via via più lenti, ma anche più capienti e meno costosi. In genere, la cache di primo livello (L1) si trova integrata nel chip della CPU (il core), la cache L2 si trova sulla
scheda della CPU ed una eventuale cache di terzo livello viene posta nei pressi della CPU.
2. Canalizzazione
2.1 Introduzione
Come visto in precedenza, le macchine di Von Neumann hanno bisogno di tre fasi per arrivare a fare quello che è richiesto da una istruzione: caricamento, decodifica, esecuzione.
Ugniuna di queste fasi
richiede un certo tempo: il caricamento richiede un tempo fisso Dt1, la decodifica richiede un tempo fisso se la macchina non è microprogrammata e un tempo variabile altrimenti
Dt2, l'esecuzione richiede un tempo variabile Dt3.
Ogni volta che una istruzione deve essere eseguita, occorre un tempo Dt=Dt1+Dt2+Dt3.
L'idea alla base della canalizzazione o pipelining è di far eseguire queste fasi contemporaneamente.
2.2 Applicazione
Progettando il calcolatore in
modo tale che le varie fasi non interferiscano l'una con l'altra, è possibile iniziare il caricamento dell'istruzione successiva mentre si stà ancora decodificando l'istruzione
corrente.
Ciò è possibile progettando macchine in cui le tre fasi richiedano lo stesso tempo, altrimenti i tempi non si incastrerebbero bene [macchine RISC]. Grazie alla canalizzazione, al
tendere ad infinito delle istruzioni eseguite, si riduce il tempo di un terzo (tre volte più veloce).
Ultime osservazioni: in realtà il tempo diminuisce di uno fratto il numero di fasi, quindi di 1\3 nel nostro caso; esistono tuttavia calcolatori con molte più fasi, così da
avere una riduzione più marcata di tempo (computer vettoriali, studiati ad Architetture III); anche in questo caso, il meccanismo di pipelining è trasparente al programmatore
assembly; anche in questo caso, quando si hanno istruzioni di salto il metodo perde la sua efficacia perchè l'istruzione successiva già acquisita non serve più; questo
problema si avverte ancora di più sui calcolatori con molte fasi (bisogna svuotare la pipeline di tutte le istruzioni che ci si stanno trattando in anticipo), ecco perchè sono stati
introdotti algoritmi di branch prediction, ovvero di previsione dei salti.
3. Memoria Virtuale
3.1 Introduzione
Può capitare di dover eseguire un programma più grande della memoria centrale disponibile. Quando ciò avviene, il programma non può essere caricato in memoria per
essere eseguito. Per risolvere tale problema, è stata ideata la memoria virtuale.
L'idea di base è la seguente: innanzitutto serve una memoria diversa dalla RAM per contenere quella parte di programma che non entra, in genere viene utilizzata una periferica come ad
esempio una unità disco; quando il programma deve essere eseguito, si carica nella RAM solo la parte che entra, mantenendo il resto sulla periferica.
Questo principio deve essere implementato in due modi diversi, a seconda che la memoria indirizzabile sia minore o maggiore della memoria fisica. Come già visto, infatti, per indirizzare
la memoria si usano stringhe numeriche di n bit, dove in genere n coincide con la dimensione di una word. In questo modo si possono indirizzare 2n celle. Esistono tre
eventualità: la memoria fisica ha N=2n celle, la memoria fisica ha N>2n celle (più celle di quante se ne possono indirizzare), la memoria fisica ha
N<2n celle (meno celle di quante se ne possono indirizzare). Tenendo presente che il primo caso si può trattare sia come il secondo che come il terzo, rimane da vedere come
realizzare una memoria virtuale negli ultimi due casi.
3.2 Overlay
Si supponga che N>2n.
In questo caso, il programma
più grande della memoria deve essere spezzato in un main che richiami subroutines.
In memoria viene caricato un ramo alla volta. Viene caricato ad esempio il ramo Main-S1-S2-S4.
Finchè si lavora su quel ramo non si hanno problemi. Se però da questo ramo bisogna passare a S5, per arrivarci occorre togliere dalla RAM S4 e metterci S5. Se invece bisogna
andare ad S7, è necessario togliere S4 ed S2 per mettere S3 ed S7. Per andare infine ad S15, sarebbe stato necessario togliere S4, S2, S1 per mettere S12, S13 ed S15.
In questo caso, con togliere e mettere si possono intendere due azioni distinte: togliere e mettere in un'altra pagina o, se tutte le pagine sono occupate, togliere e mettere sull'unità
disco.
Perchè tutto funzioni occorre che:
-
il codice contenuto in un ramo non occupi più di una pagina (2n celle); se ciò non si verifica, basta suddividere ulteriormente in programma in subroutines,
finchè tutti i rami entrano;
-
le chiamate a subroutines siano ad albero (non ci possono essere chiamate tra fratelli);
-
il linker deve essere scritto in modo da generare un eseguibile per ogni ramo
Per questo, la memoria virtuale ad overlay (che significa sovrapposizione) permette di eseguire programmi lunghi a piacere ma non è trasparente al programmatore assembly, perchè
questo deve scrivere un opportuno linker.
3.2 Swapping
Si supponga che N<2n.
In questo caso il programmatore assembly non si preoccupa che N<2n, ma sfrutta tutti i 2n indirizzi possibili, anche se questi non corrispondono a nessuna cella. Questi
indirizzi scritti dal programmatore assembly prendono nome di indirizzi virtuali.
Si supponga, ad esempio, di trovarsi nel caso in cui un programma di 2MB debba essere caricato in una RAM da 1 MB per essere eseguito.
Come prima cosa, la macchina carica quel che può: il primo MB.
I problemi nascono quando il programmatore effettua un salto ad un'etichetta presente nel secondo MB, oppure quando si arriva all'ultima istruzione del primo MB. Quando ciò avviene, si ha
una interruzione interna perchè ci si è riferiti ad un indirizzo inesistente. Si ha quindi un salto al programma di servizio che toglie il MB già caricato, salvandolo su
disco, e carica il rimanente MB al suo posto.
Ciò comporta due problemi. Il primo è che gli indirizzi del secondo MB saranno tutti sballati. Il secondo è che scaricate tutto quello che c'era in memoria e caricare tutto
quello che non entrava è molto inefficiente.
Riguardo alla prima questione, basta una minima modifica harware: bisogna inserire un registro di offset che agisca sul P.C. . Si ha che
indirizzo virtuale = indirizzo fisico (valore del P.C.) + offset (numero nel registro di offset)
L'offset è di 0 MB quando ci si trova nel primo MB ed è di 1 MB quando viene caricato il secondo pezzo.
Riguardo alla seconda questione, lo scaricamento ed il caricamento della RAM non avvengono su tutta la memoria, ma su pezzi detti pagine. Si dice quindi che la memoria è paginata e
l'operazione che carica e scarica pagine fra disco e memoria centrale è detta swapping. dire che una memoria ha una struttura a pagine è completamente diverso dal dire che è paginata; sono due concetti indipendenti l'uno
dall'altro.
La tecnica dello swapping non è trasparente al programmatore assembly, perchè questo deve scrivere un opportuno programma di servizio.