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

CacheFra 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, Cachema 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

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

ConProgettando 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.
SubroutinesIn questo caso, il programma più grande della memoria deve essere spezzato in un main che richiami subroutines. SubroutinesIn 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: 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. ATTENZIONE: queste pagine non hanno niente a che vedere con le pagine di memoria sinora trattate: 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.