Come
spiegato nella sezione 1, la riduzione della latenza della memoria
è un passo critico nell’aumentare le prestazioni di un processore.
Questa sezione discute alcune delle tecniche più comuni utilizzate
per ridurre la latenza.
2.1 Interfaccia
a larga banda con la cache secondaria
Un’interfaccia
ideale cache secondaria-processore dovrebbe essere sempre in
grado di ricevere una richiesta di dati dal processore e di
soddisfare questa richiesta nel ciclo di clock successivo; ci
si riferisce a questo comportamento come uno "zero wait state".
Per progettare una cache secondaria che sia in grado di raggiungere
questo tipo di prestazioni, l’interfaccia deve essere studiata
in modo da riuscire a trasmettere i dati sempre alla massima
velocità possibile.
Il
bus dati e il bus indirizzi per molti processori costituiscono
l’interfaccia con l’intero sistema: il processore può accedere
a qualsiasi tipo di dispositivo in ogni momento.
Quando
avviene un cache miss, viene spedito sul bus un indirizzo, e
si accede alla cache secondaria, trasferendo i dati richiesti
alla cache on-chip.
Se
avviene un cache miss in un bus di sistema condiviso, e il processore
sta utilizzando il bus esterno per leggere o scrivere su qualche
altro dispositivo, l’accesso alla cache secondaria deve attendere
finchè non si sono liberati i bus dati e indirizzi; ciò può
richiedere molti cicli di clock, a seconda della periferica
a cui si accede.
In
un sistema con bus dedicato i bus dei dati, degli indirizzi
e di controllo per la cache secondaria sono separati dai bus
che si interfacciano con il resto del sistema. In questo modo
gli accessi alla cache secondaria in caso di miss vengono garantiti
sempre, qualsiasi cosa stia facendo il sistema.
La
figura 3 mostra un diagramma a blocchi di un’interfaccia condivisa
e di una separata.
Fig.
3
2.2 Accesso
a blocchi
Quando
avviene un cache miss on-chip, esiste un numero di byte, solitamente
programmabile, che viene trasferito sul bus ogni volta che si
accede alla cache secondaria. Questo numero è la dimensione
di una linea di cache; per le architetture attuali la dimensione
classica è di 32 byte.
Il
numero di accessi richiesto per effettuare il riempimento di
una linea dipende dalla dimensione del bus di dati esterno del
processore. Per esempio, un processore con bus dati a 64 bit
che si interfaccia con una memoria a 64 bit impiegherà quattro
accessi alla cache secondaria per riempire una linea di cache
da 32 byte. Per completare tutto ciò il processore deve generare
quattro indirizzi separati e guidare ognuno sul bus indirizzi
esterno tramite appropriati segnali di controllo.
Utilizzando
l’accesso a blocchi il processore deve invece generare solo
l’indirizzo iniziale della sequenza, mentre gli altri tre indirizzi
vengono generati dalla logica di controllo della cache.
2.3 Interleaving
L’interleaving
è una tecnica di progetto utilizzata per aumentare la larghezza
di banda della memoria; tale tecnica può essere applicata sia
alla cache secondaria che alla memoria principale.
Il
sistema di memoria più semplice è quello con un solo banco di
memoria. Quando si accede a tale banco, deve trascorrere un
certo intervallo temporale prima di un secondo accesso; quaesto
intervallo dipende sia dal progetto del sistema sia dalla velocità
dei dispositivi di memoria utilizzati. Se sono presenti più
banchi, allora l’accesso ai banchi può essere sovrapposto. L’abilità
di sovrapporre tali accessi aiuta a nascondere le latenze della
memoria e diventa sempre più importante al crescere della dimensione
dei dati richiesti.
Una
tipica memoria con interleaving consiste in banchi pari e dispari.
Per esempio, il processore richiede dei dati ad un indirizzo
pari, così il controller della memoria inizia un ciclo al banco
pari. Una volta che l’indirizzo è stato raggiunto dalla logica
di controllo della memoria, il processore può generare un nuovo
indirizzo, il più delle volte nel ciclo di clock successivo.
Se il nuovo indirizzo si riferisce ad un banco dispari, l’accesso
alla memoria può iniziare immediatamente; in questo modo, non
appena il banco pari ha completato l'operazione, il banco dispari
è già pronto a fornire il dato. Pertanto più a lungo si riesce
a minimizzare gli accessi sequenziali, più ci si avvicina alle
prestazioni di tipo "zero wait state".
I più
comuni sistemi di memoria con interleaving sono quelli a due
e a quattro vie; il numero di banchi e la larghezza di banda
di ciascuno sono spesso determinati dal processore.
2.4 Cache
non bloccante
In
una tipica implementazione il processore agisce sulla cache
finchè avviene un cache miss. A questo punto, trascorre un certo
numero di cicli prima che i dati vengano riportati nella cache
on-chip, permettendo la ripresa dell’esecuzione. Questo tipo
di implementazione è detto bloccante, dato che non si può accedere
alla cache finchè non viene risolto il cache miss.
La
cache di tipo non bloccante invece, permette accessi consecutivi
anche in caso di cache miss. In questo caso, per aumentare le
prestazioni globali del sistema, è cruciale localizzare il prima
possibile i miss e effettuare i passi necessari per risolverli.
La fig. 4 mostra un esempio di come le cache bloccanti e non
bloccanti reagiscono a miss multipli.
Fig.
4
2.5 Prefetch
Il
prefetching è una tecnica con cui il processore può richiedere
un blocco di cache prima del momento in cui è effettivamente
necessario. L’istruzione di prefetch deve essere integrata nel
set di istruzioni, e deve esserci un appropriato hardware per
eseguirla.
Per
esempio, supponiamo che il compilatore stia avanzando in modo
sequenziale attraverso un segmento di codice. Il compilatore
può fare l’ipotesi che questa sequenza continuerà oltre il range
degli indirizzi disponibili nella cache di primo livello, e
può richiedere un’istruzione di prefetch, la quale carica il
blocco di istruzioni successivo nella cache di secondo livello.
Quindi, quando il processore richiede la sequenza successiva,
questa può essere eseguita ad una frequenza maggiore; se per
qualche motivo tale blocco non è necessario, l’area nella cache
secondaria viene semplicemente sovrascritta da altre istruzioni.
Il
prefetching permette quindi al compilatore di anticipare la
necessità di un dato blocco, e di piazzarlo il più possibile
vicino alla CPU.
DIPENDENZE
FRA I DATI
Per
ridurre l’impatto negativo sulle prestazioni delle dipendenze
fra i dati vengono utilizzate due tecniche, discusse qui di
seguito.
2.6
Ridenominazione dei registri
La
ridenominazione dei registri distingue fra registri logici e
registri fisici; i registri logici sono mappati dinamicamente
nei registri fisici attraverso apposite tabelle che vengono
aggiornate ogni volta che un’istruzione viene decodificata.
Ogni nuovo risultato viene scritto in un registro fisico; tuttavia,
il contenuto precedente di ogni registro logico viene salvato,
e può essere recuperato nel caso l’istruzione debba essere abortita
a causa di un’eccezione o di una previsione di salto non corretta.
Mentre
il processore esegue le istruzioni, vengono generati moltissimi
risultati temporanei, i quali sono immagazzinati in appositi
registri. I valori temporanei diventano permanenti quando la
corrispondente istruzione viene "graduata", cioè quando tutte
le istruzioni precedenti sono state completate con successo
nell’ordine del programma.
Il
programmatore è consapevole dell’esistenza dei soli registri
logici, mentre l’implementazione dei registri fisici è nascosta.
La
ridenominazione dei registri semplifica il controllo delle dipendenze
fra i dati. In una macchina che può eseguire istruzioni fuori
ordine, i numeri dei registri logici possono diventare ambigui,
poiché ad uno stesso registro può essere assegnata una successione
di valori diversi. Ma dato che i numeri dei registri fisici
identificano in modo unico ogni risultato, il controllo delle
dipendenze non risulta più ambiguo.
2.7 Esecuzione
fuori ordine
In
un tipico processore che esegue le istruzioni in ordine, ogni
istruzione dipende dall’istruzione precedente che produce i
suoi operandi, e l’esecuzione non può iniziare finchè questi
operandi non diventano validi. Se gli operandi richiesti per
eseguire una data istruzione non sono validi, la pipeline stalla
finchè tali operandi non diventano disponibili. Poiché le istruzioni
vengono eseguite rispettando l’ordine del programma, solitamente
gli stalli ritardano tutte le istruzioni seguenti.
In
una macchina superscalare "in-order", dove vengono eseguite
più istruzioni per ciclo, varie istruzioni consecutive possono
iniziare simultaneamente l’esecuzione solo se tutti i loro corrispondenti
operandi sono validi, altrimenti il processore và in stallo.
In
una macchina superscalare "out-of-order", ogni istruzione
può iniziare la sua esecuzione non appena gli operandi necessari
diventano disponibili, senza riguardo per la sequenza originaria.
L’hardware effettivamente riarrangia l’ordine delle istruzioni
per tenere sempre occupate le varie unità di esecuzione. Questo
processo viene chiamato dynamic issuing.
PREDIZIONE
DEI SALTI
2.7 BPU
(Brench Prediction Unit)
Come già detto nella sezione 1.3, le diramazioni interrompono
il flusso della pipeline; pertanto, per minimizzare il numero
di interruzioni, sono necessari degli schemi di branch prediction.
Le diramazioni accadono frequentemente, in media ogni sei istruzioni;
in un’architettura superscalare, dove vengono eseguite anche
quattro istruzioni per ciclo, la predizione di tali diramazioni
diventa importante.
Molti
schemi di predizione utilizzano degli algoritmi che tengono
traccia del comportamento delle diramazioni l’ultima volta che
sono state eseguite. Per esempio, se il circuito che memorizza
tali comportamenti mostra che la volta precedente un’istruzione
ha preso la diramazione, allora si fa l’ipotesi che questa venga
presa ancora. Un’implementazione hardware di questa assunzione
significa che il programma invierà allo stesso indirizzo tutte
le successive diramazioni. La pipeline ora contiene un’istruzione
di salto condizionale e altre istruzioni successive ma in quel
momento non si sa se tali istruzioni verranno eseguite; infatti
se la diramazione non è stata predetta correttamente, le istruzioni
nella pipeline devono essere abortite.
Molte
architetture implementano un branch stack nel quale vengono
salvati gli indirizzi alternativi. Se si prevede che la diramazione
non sarà presa, viene salvato l’indirizzo dell’istruzione di
branch; in caso contrario viene salvato l’indirizzo immediatamente
seguente a tale istruzione.