Architetture dei processori/Unità di decodifica

L'unità di decodifica si occupa di ricevere le istruzioni in ingresso e di attivare le opportune unità interne del processore per eseguire l'istruzione caricata. Nei primi processori questa unità provvedeva a tradurre l'istruzione in opportuno microcodice che veniva utilizzato per gestire l'ALU, i registri, l'accumulatore e le altre unità funzionali dei processori. Con l'avvento delle unità dotate di pipeline il microcodice è stato sostituito da segnali di controllo che temporizzano le varie unità.

Particolare di un microprocessore

Alcuni microprocessori comunque utilizzano ancora parzialmente il microcodice per codificare alcune istruzioni che vengono considerate poco utilizzate ma che devono essere mantenute per ragione di compatibilità. Questa strategia permette di risparmiare transistor ma ovviamente produce un degrado delle prestazioni.

Molti microprocessori moderni sono progettati per eseguire set di istruzioni molto complessi, sviluppati per processori CISC. Questo set di istruzioni renderebbe difficile l'esecuzione di più istruzioni contemporaneamente. Quindi molti microprocessori traducono le istruzioni complesse in sequenze di istruzioni più semplici da eseguire rendendo il set di istruzioni simile a quello di un'architettura RISC. Anche questo lavoro viene svolto dall'unità di decodifica.

Nei microprocessori superscalari l'unità di decodifica diventa una componente critica del processore dato che si occupa di ricevere le istruzioni e di organizzarle in modo da ridurre al minimo possibile gli stalli delle pipeline. Questo viene ottenuto eseguendo le istruzioni fuori ordine, ridenominando i registri, prevedendo le condizioni di salto, precaricando e dati ed implementando l'esecuzione speculativa. Non tutti i processori implementato tutte queste tecniche dato che alcune sono complementari ed in alcuni casi l'implementazione di queste tecniche sarebbe molto onerosa e porterebbe modesti miglioramenti delle prestazioni.

Esecuzione fuori ordine modifica

 
Schema concettuale dell'esecuzione fuori ordine. Il programma viene caricato come istruzioni seriali, le istruzioni vengono analizzate, riordinate tenendo conto delle dipendenze, eseguite in parallelo, ed riordinate prima di provvedere al salvataggio dei dati in memoria.

L'esecuzione fuori ordine indica la capacità di molti processori di eseguire le singole istruzioni senza rispettare necessariamente l'ordine imposto dal programmatore. Il processore in sostanza analizza il codice che dovrà eseguire, individua le istruzioni che non sono vincolate da altre istruzioni e le esegue in parallelo con il codice principale. La rilevazione delle dipendenza tra le varie istruzioni è un compito complesso dato che le istruzioni possono essere vincolate in vari modi. Per esempio se ho un'istruzione che esegue una somma tra due registri e poi il risultato della somma viene utilizzato come operando per una divisione va da se che la divisione e la somma non possono essere eseguite in parallelo senza generare conflitti. Il problema può essere risolto replicando delle unità funzionali (vedi sezione sulla pipeline) oppure eseguendo in parallelo all'istruzione di somma un'istruzione che non ha dipendenze e rimandando la divisione.

In generale queste unità funzionali decodificano le istruzioni e pongono le istruzioni che non hanno vincoli (o che hanno vincoli che sono stati risolti) dentro un buffer che alimenta le pipeline. Dopo le pipeline vi è un'unità che utilizzando delle etichette collegate alle istruzioni ricostruisce l'ordine cronologico delle istruzioni facendo uscire dal processore i risultati delle istruzioni con l'ordine cronologico impostato dal programma. Questa unità inoltre provvede ad eliminare le eventuali operazioni eseguite erroneamente dal processore. La presenza di unità di predizione dei salti implica che il processore spesso esegue delle istruzioni presupponendo che il processore esegua (o non esegua) un certo salto. Ma, se la previsione fornita dall'unità di predizione dei salti si dovesse dimostrare non corretta le istruzioni eseguite erroneamente vanno eliminate per preservare il corretto funzionamento del programma.

Ridenominazione dei registri modifica

Nei processori che implementano l'esecuzione fuori ordine non vi è una reale corrispondenza tra ciò che il programmatore si aspetta di trovare nei registri e ciò che realmente si trova nei registri dato che l'ordine cronologico del programma non è rispettato. Quando un'istruzione di trova ad accedere ad un registro che non contiene un dato corretto per il programma la pipeline dovrebbe andare in stallo fino a quando il registro non contenga il dato corretto o fino a quando non sia libero. Per evitare questo spreco di cicli di calcolo i processori implementano la ridenominazione dei registri. In sostanza il processore dispone di molti registri fisici nascosti che vengono assegnati alle istruzioni quando servono dei registri che risultano già occupati. Quindi in un dato istante per esempio un ipotetico registro R0 potrebbe essere in realtà assegnato a tre registri fisici che fingono di essere il registro R0 per le istruzioni che li usano. La corrispondenza tra questi registri fisici e i registri visti dal programma viene mantenuta da una tabella che viene aggiornata ad ogni decodifica di un'istruzione.

Predizione dei salti modifica

Un processore basato su pipeline viene influenzato molto negativamente dalla presenza di salti condizionati dato che questi possono costringere il processore a svuotare la pipeline. Mediamente ogni 6 istruzioni un processore basato su architettura x86 incontra un salto condizionato e quindi i processori implementano delle unità molto complesse per cercare di prevedere se il salto condizionato verrà eseguito in modo da caricare in anticipo il blocco di istruzioni corretto nella pipeline. Vista l'importanza di questa unità si rimanda al capitolo apposito per un'analisi approfondita.

Precaricamento dei dati modifica

I processori sono diventati sempre più veloci e nel giro di un decennio sono passati da frequenze di poche decine di Megahertz a frequenze di funzionamento dell'ordine dei Gigahertz. Le memorie invece non sono riuscite a stare al passo pur aumentando notevolmente la quantità di dati trasmessi per secondo. I processori per attutire il problema hanno implementato cache di primo, secondo e a volte anche di terzo livello. La presenza delle cache con gli ultimi processori non è più sufficiente per evitare un eccessivo deperimento delle prestazioni e quindi alcuni processori implementano delle unità che analizzano il codice cercando di prevedere in anticipo quali dati o quali istruzioni serviranno al processore e provvedendo al loro caricamento in cache (o direttamente nel processore) prima della loro reale necessità. Alcune architetture prevedono delle istruzioni apposite per indicare quali blocchi precaricare ma la maggior parte delle architetture non sono dotate di queste istruzioni e quindi devono basarsi solamente sull'ispezione del codice in tempo reale.

Il precaricamento dei dati e delle istruzioni introduce dei problemi, per esempio se il microprocessore carica delle istruzioni dipendenti da un salto e quel salto non viene eseguito il processore deve provvedere ad eliminare le istruzioni caricate erroneamente prima di caricare le istruzioni da eseguire. Problemi anche maggiori si hanno nel caso di generazione di un'eccezione, per esempio l'accesso a una locazione non consentita genera una eccezione che va segnalata al sistema operativo, ma la segnalazione va effettuata quando dovrebbe effettivamente avere luogo e non prima per via del precaricamento. Il precaricamento dei dati invece deve garantire la coerenza e la validità dei dati quindi se un dato viene precaricato nessuna istruzione deve modificarlo prima del suo effettivo utilizzo da parte delle istruzioni che hanno forzato il precaricamento. Questi problemi rendono il precaricamento dei dati e delle istruzioni molto complesso da implementare in hardware senza un supporto diretto del set di istruzioni. Invece se il set di istruzioni supporta nativamente questa caratteristica la sua gestione diventa molto più semplice.

Esecuzione predicativa modifica

L'esecuzione predicativa è un approccio che mira a ridurre la dipendenza del processore dall'unità di predizione delle diramazioni. I microprocessori moderni sono dotati di molte unità funzionali in grado di eseguire operazioni in parallelo ma queste unità sono quasi sempre vuote. Processori anche molto complessi come il Pentium 4 pur potendo in teoria eseguire fino a 6 operazioni in contemporanea in realtà per la maggior parte del tempo eseguono una o due operazioni in parallelo. Partendo da questa constatazione l'approccio predicativo punta a riempire al massimo le unità di elaborazione eliminando le istruzioni eseguite ma non necessarie. Un processore dotato di esecuzione predicativa oltre a leggere le istruzioni legge dei predicati associate a esse, questi predicati indicano al processore in caso di salto condizionato quale ramo risulta effettivamente valido. In sostanza in caso di salto condizionato il processore sostituisce la condizione di salto con un predicato (per esempio il predicato P0). Al blocco di istruzioni da eseguire se il salto viene eseguito viene associato il predicato P0=0 mentre al blocco di istruzioni da eseguire nel caso il salto non venga eseguito viene associato il predicato P0=1. Poi il processore esegue in parallelo le istruzioni del primo blocco in una un'unità funzionale e l'istruzione del secondo blocco in un'altra unità funzionale. L'istruzione di salto in sostanza è stata convertita nel predicato che vale 0 oppure 1 e quindi il processore può stabilire quale blocco di istruzioni sia valido semplicemente eseguendo le istruzioni e calcolando il valore di P0 come calcola il valore di ogni operando. Il blocco con le istruzioni non valide viene individuato tramite il predicato loro associato ed eliminato. Questa filosofia di sviluppo quindi preferisce eseguire anche istruzioni inutili pur di mantenere sempre piene le pipeline.

Questa metodologia di esecuzione fornisce buone prestazioni se le pipeline non sono formate da troppi stadi. Per esempio un processore come il Pentium 4 è dotato di pipeline da 31 stadi e quindi un'esecuzione predicativa potrebbe portare ad eseguire fino a 31 operazioni inutili prima dell'individuazione del valore di P0 e quindi dell'individuazione delle istruzioni eseguite erroneamente. Il processore Itanium 2, che implementa questa modalità di esecuzione, difatti utilizza pipeline corte, a 8 stadi.

L'esecuzione predicativa se non è gestita dal set di istruzioni del processore è molto costosa da implementare. Il processore dovrebbe associare in tempo reale alle istruzioni i vari predicati e tenerne traccia durante l'esecuzione. È da notare che non tutti i salti possono convenientemente essere risolti con i predicati, a volte conviene basarsi sull'unità di predizione dei salti, difatti se il salto coinvolge un blocco con molte istruzioni e si ritiene di poter calcolare con buona probabilità l'esito del salto non conviene utilizzare l'esecuzione predicativa. Quale delle due alternative scegliere dipende dal codice e solo un'analisi di esso permette di individuare l'alternativa migliore, ma un processore, dovendo scegliere in tempo reale tra le alternative, dovrebbe far affidamento su delle euristiche che non sempre danno un risultato ottimale. Invece un processore dotato nativamente di questa modalità di esecuzione deve semplicemente caricare i predicati e regolare correttamente i registri appositi dato che tutta la fase di analisi del codice è stata svolta precedentemente dal compilatore che non avendo problemi di tempo può individuare l'alternativa migliore. Un esempio classico si ha nel caso dei cicli, in un ciclo cha va da 1 a 1000 per esempio un compilatore comprenderebbe che l'esecuzione predicativa non è la scelta ottimale dato che farebbe eseguire per mille volte del codice in più. Invece, se il set di istruzioni lo consente, il compilatore potrebbe indicare al processore che il salto non verrà eseguito permettendo al processore di prevedere per 999 volte la diramazione corretta e sbagliando solo una volta. Invece nel caso di molti IF annidati con condizioni di salto che dipendono da dati esterni e con poche istruzioni controllate dagli IF la strategia predicativa è la migliore soluzione dato che elimina la possibilità di errore ed è in grado di gestire agevolmente anche più condizioni di salto contemporaneamente. Va detto comunque, che in generale l'esecuzione predicativa fornisce risultati peggiori di una buona unità di predizione dei salti. Difatti quasi tutti i processori implementano unità di predizioni dei salti mentre l'esecuzione predicativa è molto meno utilizzata.

Ovviamente tutte queste infrastrutture aggiuntive rendono le unità di decodifica molto complesse e queste occupano buona parte dei transistor dei moderni processori. Per ridurre il problema si sono sviluppate architetture come le architetture VLIW e derivate che affrontano il problema alla radice con un paradigma diverso eliminando alcune unità funzionali e semplificandone altre.