Appunti del corso di ARCHITETTURA DEGLI ELABORATORI

Autore: professor Giovanni Chiola; email: chiola@disi.unige.it
Copyright © 1996-1998.



Indice




Un esempio di RISC: la famiglia SPARC

In questo capitolo vedremo come i concetti tipici della strutturazione RISC di una macchina convenzionale sono stati messi in pratica per la definizione e realizzazione dell'architettura SPARC (acronimo che sta per "Scalable Processor ARChitecture"). Col nome SPARC si intende una "famiglia" di processori di struttura simile a livello di microarchitettura e di definizione della macchina convenzionale, ma caratterizzati da tecnologie realizzative, costi e prestazioni notevolmente differenziati tra loro. La prima versione dello standard SPARC venne introdotto da SUN Microsystems nel 1987, riprendendo ed estendendo la struttura dei processori sperimentali RISC-1, RISC-2 e SOAR, proposti dal gruppo di ricercatori guidati dal professor Patterson presso la University of California at Berkeley.

La realizzazione fisica dell'architettura SPARC prevede l'interconnessione di 3 unità funzionali separate per la realizzazione della CPU:

IU:
Integer Unit. É l'unità centrale di elaborazione vera e propria, contenente le unità di fetch, e decodifica di tutte le istruzioni, ed il data-path corrispondente ai dati in rappresentazione fixed point.
FPU:
Floating-Point Unit. É l'unità ausiliaria contenente il data-path corrispondente ai dati in rappresentazione floating-point.
CP:
User-supplied Coprocessor. É un'unità ausiliaria della quale l'architettura SPARC specifica solo l'interfaccia con le altre unità. Costruttori diversi possono associare Coprocessori diversi a processori della famiglia SPARC (es., Digital Signal Processors, unità vettoriali, dispositivi di comunicazione ad alte prestazioni, ecc.)
FPU e CP sono connessi ad IU, dalla quale ricevono i segnali di controllo per il loro funzionamento. IU decodifica quindi tutte le istruzioni, quindi esegue direttamente le istruzioni di manipolazione dei dati di tipo intero e di controllo del flusso di esecuzione del programma, mentre demanda alle unità FPU e CP l'esecuzione delle altre istruzioni. Nel seguito della descrizione ci occuperemo prevalentemente dell'IU, e faremo solo qualche sporadico accenno alle caratteristiche della FPU.

L'architettura SPARC nacque come architettura a 32 bit. L'ultima versione dello standard (V9, nota col nome commerciale "UltraSPARC") definisce invece una architettura basata su registri a 64 bit di tipo superscalare a 4 vie. Come tutte le architetture RISC, SPARC sfrutta pesantemente la tecnica di Pipelining: la prima versione prevedeva un pipeline a 4 stadi (Fetch, Decode, Exec, Write); la versione corrente (V9) prevede l'uso di 9 stadi di pipeline.



Principali funzionalità

Prima di passare ad esaminare l'insieme delle istruzioni della macchina convenzionale ed il relativo codice di rappresentazione in memoria, ci soffermeremo ad esaminare alcune caratteristiche peculiari di SPARC, legate alla strutturazione dei registri nel data-path dell'IU ed all'uso efficiente del meccanismo di pipelining per l'esecuzione di programmi non sequenziali.

Register File e Overlapping Sliding Window

L'unità IU dell'architettura SPARC contiene un numero molto elevato di registri (variabile tra una realizzazione e l'altra) fino ad un massimo di 520 registri (a 64 bit nella versione V9, oppure a 32 bit nelle versioni precedenti). Tuttavia, in ogni momento risultano essere accessibili mediante istruzioni della macchina convenzionale solo 32 registri. Dei 32 registri utlizzabili simultaneamente, 8 (detti registri globali, in inglese Global) sono sempre accessibili in ogni circostanza da ogni programma in esecuzione. Gli 8 registri globali vengono identificati indifferentemente con i simboli R0, R1, ..., R7 oppure G0, G1, ... G7.

I rimanenti 24 registri accessibili variano invece a seconda del programma in esecuzione e del "livello di annidamento" di chiamate di sottoprogramma. Vengono ulteriormente suddivisi in 3 sottoinsiemi di 8 registri, chiamati di ingresso (Incoming), di uscita (Outgoing) e locali (Local). Gli 8 registri locali, individuati indifferentemente dai simboli R16, R17, ..., R23 oppure L0, L1, ..., L7, sono registri accessibili solo dalla procedura attualmente in fase di esecuzione sul processore. Quando il processore esegue una istruzione CALL per l'attivazione di un sottoprogramma, l'insieme degli 8 registri individuati dai simboli L0, L1, ..., L7 cambia, in modo che la procedura possa usare un sottoinsieme diverso di registri, senza alterare il contenuto dei registri locali del programma chiamante. Il programma chiamante ritroverà automaticamente "il suo sottoinsieme" di registri locali dopo l'esecuzione dell'istruzione di "ritorno da sottoprogramma".

Anche gli altri due sottoinsiemi di 8 registri (di ingresso e di uscita) cambiano a seguito dell'attivazione di un sottoprogramma, ma vengono "condivisi" tra programma chiamante e procedura chiamata, in modo da consentirne l'utilizzazione per il "passaggio di informazioni". In particolare, l'insieme dei registri di ingresso della procedura chiamata (individuati dai simboli R24, R25, ..., R31 oppure indifferentemente I0, I1, ..., I5, FP, I7) vengono fatti coincidere coi registri di uscita del programma chiamante (individuati dai simboli R8, R9, ..., R15 oppure indifferentemente O0, O1, ..., O5, SP, O7).

Il meccanismo che realizza l'accesso ai 24 registri "non globali" é intuitivamente simile a quello di una feritoia praticata su un "coperchio scorrevore". Il coperchio impedisce di vedere la totalità dei registri effettivamente presenti nell'IU; dalla feritoia praticata nel coperchio se ne vedono solo 24. Il coperchio viene mantenuto fermo durante l'esecuzione di una procedura fin quando non si incontra una istruzione di chiamata di sottoprogramma. A quel punto il coperchio viene fatto scorrere "verso l'alto" di 16 posizioni. In questo modo dalla feritoia si continueranno a vedere 24 registri, ma questi corrisponderanno solo in parte con i 24 visibili prima dell'operazione di scorrimento. In particolare, quello che prima era accessibile col nome R15 (oppure O7) continuerà ad essere accessibile ma col nuovo nome R31 (oppure I7). Solo 8 registri continueranno ad essere accessibili dopo lo scorrimento (gli attuali registri di ingresso, precedentemente accessibili come registri di uscita), mentre gli altri 16 (gli attuali registri locali e di uscita) corrisponderanno a registri precedentemente non visibili. Il contesto per l'esecuzione del programma chiamante viene ripristinato mediante una corrispondente operazione di "scorrimento verso il basso" del coperchio di 16 posizioni, al momento dell'esecuzione della istruzione RET.

Notare che il registro di uscita R14 ed il registro di ingresso R30 hanno un uso particolare, evidenziato dall'uso dei simboli SP (invece di O6) e FP (invece di I6). Così come nella macchina convenzionale "giocattolo" VM-2, i registri SP (stack pointer) ed FP (frame pointer) vengono usati per delimitare la zona di memoria del segmento stack usata dal programma per mantenere copia delle variabili locali alla procedura in fase di esecuzione. In effetti il register file usato con la modalità a "finestra mobile parzialmente sovrapposta" sostituisce in parte la struttura stack memorizzata in RAM. La procedura in esecuzione può sfruttare i registri locali per mantenere i valori di variabili locali, ed i registri di ingresso e di uscita per "passare parametri" a sottoprogrammi, senza bisogno di utilizzare la struttura stack in RAM (che presenta lo svantaggio di un maggior tempo di accesso rispetto ai registri interni alla CPU). Tuttavia il numero di registri accessibili nella finestra mobile (24 nel caso SPARC) costituisce una limitazione. Se un programma richiede l'uso di più di 8 variabili locali, gli 8 registri locali della CPU non sono sufficienti a soddisfare le esigenze; pertanto le variabili locali eccedenti il numero disponibile di registri locali devono essere "allocate" nella struttura stack in memoria RAM. In altre parole, possiamo considerare i registri della finestra mobile come una ottimizzazione della struttura stack, che tende a favorire i programmi che utilizzano al più 8 variabili locali in termini di velocità di esecuzione, facendo si che questi possano essere organizzati in modo da usare solo registri interni alla CPU, senza necessità di ricorrere a memorizzazione di valori nella struttura stack in RAM. Notare infine come la sostituzione del valore del registro FP col valore precedentemente contenuto nel registro SP a seguito dell'attivazione di un sottoprogramma venga ottenuta in modo astuto ed efficace facendo corrispondere il registro FP dopo lo scorrimento della finestra mobile con lo stesso registro fisico che era precedentemente accessibile come registro SP (evitando quindi qualsiasi operazione di copia dei dati contenuti nei registri). Lo scorrimento in basso ripristina i valori precedenti dei registri SP ed FP in modo altrettanto astuto ed efficace.

Anche il registro I7 ha un uso particolare nell'architettura SPARC: viene infatti usato per mantenere l'indirizzo "di ritorno" dalla chiamata di una procedura. Pertanto una procedura in fase di esecuzione non deve cambiare il contenuto del registro I7, altrimenti non potrebbe poi completare correttamente l'esecuzione dell'istruzione RET. L'esecuzione dell'istruzione CALL provoca la copia del contenuto del registro PC (program counter) nel registro O7; la successiva operazione di scorrimento della finestra mobile di 16 posizioni verso l'alto provoca un cambiamento della denominazione del registro contenente l'indirizzo di ritorno (che passa ad essere individuato come I7). L'esecuzione dell'istruzione di ritorno da sottoprogramma (RET) consiste quindi nel ricopiare il contenuto del registro I7 nel registro PC, e nella successiva oprazione di scorrimento verso il basso di 16 posizioni della finestra mobile di registri. Usando questo organizzazione, anche il salvataggio ed il ripristino del valore del registro PC (insiti nel meccanismo di attivazione e terminazione di un sottoprogramma) vengono portati a termine facendo riferimento solo al contenute del register file interno alla CPU, senza coinvolgere la struttura stack in RAM (con relativi benefici in termini di tempo di esecuzione delle istruzioni CALL e RET). Questo meccanismo fa si che il registro O7 possa essere usato come "registro temporaneo" durante l'esecuzione di una procedura: il programma in fase di esecuzione può infatti memorizzare informazioni in O7, ma queste vengono "perse" nel caso di attivazione di un sottoprogramma (essendo automaticamente "ricoperte" dall'indirizzo di ritorno dal sottoprogramma).

Fino ad ora abbiamo descritto le operazioni di scorrimento della finestra mobile sul register file supponendo che fosse sempre possibile effettuare una operazione di scorrimento verso l'alto. Ovviamente, data la dimensione (grande ma) limitata del register file, se il processore esegue un numero eccessivo di operazioni di scorrimento verso l'alto prima o poi esaurisce tutti i registri, incorrendo in una situazione di errore. A livello realizzativo, le operazioni di scorrimento vengono effettuate incrementando il valore contenuto in un registro, il cui valore viene sommato alla rappresentazione su 5 bit di un numero compreso tra otto e trentuno. Quando il risultato eccede la possibilità di rappresentazione del registro, viene effettuata una operazione di troncamento "modulo 2**N" nel caso si usi un registro da N bit. L'uso di una aritmetica "modulo 2**N" può essere visualizzato come un "ripiegamento" dei registri sulla superficie di un cilindro, per cui una volta superato l'ultimo registro del register file si torna ad individuarne il primo. La situazione di errore viene gestita mediante l'attivazione di una eccezione (Trap).

Nella fattispecie, l'architettura SPARC prevede l'uso di un register file di dimensioni variabili da realizzazione a realizzazione, fino ad un massimo di 512 registri. Nel seguito di questa spiegazione faremo riferimento all'uso di un register file di dimensione massima (512 registri). Lo scorrimento di 16 posizioni in aritmetica modulo 512 implica l'individuazione di 32 posizioni diverse per la "finestra di visibilità" dei registri. La "posizione attuale" della finestra di visibilità viene pertanto codificata come un numero compreso tra 0 e 31 all'interno di un registro da 5 bit chiamato CWP. Il registro viene inizializzato al valore 31 per l'attivazione del programma principale, decrementato di uno in ogni operazione di scorrimento verso l'alto della finestra di visibilità, e decrementato di uno in ogni operazione di scorrimento verso il basso della finestra di visibilità. Ciascuna posizione della finestra di visibilità é poi associata univocamente ad un bit di un registro a 32 bit denominato WIM. La presenza del valore 1 in un bit del registro WIM fa si che il processore esegua una Trap quando il registro CWP assume il valore corrispondente alla posizione di quel bit; il valore 0 in un bit del registro WIM implica invece l'esecuzione senza interruzioni del programma quando CWP assome il valore corrispondente alla posizione di quel bit.

Inizialmente il registro WIM assume il valore 1 (in modo da associare il bit 0 a tutte le posizioni della finestra mobile tranne la posizione 0). Il programma in esecuzione può quindi eseguire per 30 volte l'operazione di spostamento della finestra di visibilità (passando quindi dal valore CWP=31 sino al valore CWP=1) senza incorrere in attivazioni di Trap. Eseguendo l'operazione di spostamento della finestra per la 31esima volta, si passa invece ad individuare la posizione CWP=0, per la quale scatta il meccanismo di attivazione di una Trap (a causa del valore 1 contenuto in WIM). Notare che la posizione 0 della finestra, a causa dell'operazione di indirizzamento "modulo 512", individua come insieme di registri di uscita lo stesso insieme già usato in precedenza come registri di ingresso per la posizione CWP=31, e quindi non é immediatamente utilizzabile per l'esecuzione di un programma utente. Per ovviare all'inconveniente la procedura di gestione della Trap ricopia il contenuto dei 16 registri di ingresso e locali della posizione CWP=31 all'interno dello stack (allocato in memoria RAM), e poi altera il valore CWP mediante una operazione di "rotazione verso destra" (scorrimento verso destra con inserzione nella posizione 31 del valore precedentemente presente nella posizione 0). Dopo l'esecuzione del Trap handler, l'esecuzione del programma utente può riprendere dal punto in cui era stata interrotta, utilizzando normalmente la posizione CWP=0.

Ogni successiva operazione di scorrimento verso l'alto della finestra di visibilità genera poi una eccezione trattata dal Trap handler nello stesso modo. Per esempio, la 32esima operazione di scorrimento porta il valore CWP=31, e questo comporta l'attivazione di una eccezione a causa del valore 2**31 contenuto in WIM. La posizione CWP=31 condivide 8 registri con la posizione CWP=30, quindi un programma che alterasse il contenuto dei registri di uscita provocherebbe una alterazione dei valori contenuti nei corrispondenti registri di ingresso della posizione CWP=30 già usata in precedenza. Il Trap Handler "corregge" la situazione salvando in 16 parole dello stack allocato in RAM i valori contenuti nei registri di ingresso e locali della posizione CWP=30, poi opera nuovamente una rotazione del contenuto del registro WIM, lasciandovi il valore 2**30 (in sostituzione del valore 2**31). Quando il programma interrotto dall'eccezione riprende la sua esecuzione normale, potrà quindi accedere ai registri di uscita della posizione CWP=31 senza perdita di informazioni per la posizione 30 già usata in precedenza.

Ad ogni scorrimento verso l'alto della finestra mobile di visibilità deve prima o poi corrispondere uno scorrimento verso il basso (realizzato decrementando di uno il valore di CWP) per terminare l'esecuzione della procedura che era stata attivata. Lo scorrimento verso il basso non comporta l'attivazione di una eccezione fin quando non si ritorna all'ultima posizione che era stata salvata nello stack. Per esempio, nel caso fossero state eseguite 32 rotazioni verso l'alto, le prime 30 rotazioni verso il basso vengono completate senza eccezioni. La 31esima rotazione verso il basso riporta CWP ad assumere il valore 30, corrispondente al bit diverso da zero nel registro WIM (che nel frattempo avrà mantenuto il valore 2**30 inserito dall'ultima esecuzione del Trap Handler), generando quindi nuovamente una eccezione. Il Trap Handler può stavolta correggere l'errore estraendo i valori dei registri locali e di ingresso precedentemente copiati nello stack e ricopiandoli nuovamente nei registri. Al termine delle operazioni di copia si applica una rotazione verso sinistra del contenuto del registro WIM (che passa quindi ad assumere il valore 2**31) e si può riprendere l'esecuzione della prima procedura con CWP=30 dal punto in cui era stata interrotta. Analogamente, il ripristino della posizione iniziale CWP=31 (corrispondente al programma principale che aveva scatenato l'attivazione di 32 procedure annidate) genera nuovamente una eccezione trattata dal Trap Handler. Questo prevede il ripristino dei valori dei registri locali e di ingresso del programma principale (prima copiati nello stack) e poi la rotazione del contenuto del registro WIM, in modo da ripristinare il valore originario 1 (in sostituzione del valore 31 che ha determinato l'eccezione).

Notare che le copie dei valori dei registri corrispondenti a posizioni da riutilizzare della finestra mobile di visibilità sul register file non possono essere inframmezzate semplicemente con le altre celle dello stack usate dal programma utente. Se il Trap Handler salvasse i registri di una finestra "in cima allo stack" il meccanismo di ripristino dei valori fallirebbe, in quanto le celle sarebbero già state disallocate dallo stack a seguito del ritorno dalla 31esima procedura (per la posizione 31) o della 32esima procedura (per la posizione 30). A questo problema si può porre rimedio in due modi diversi:

Pipelining ed esecuzione di programmi non sequenziali

La prima definizione dell'architettura SPARC prevedeva la decomposizione dell'esecuzione delle istruzioni in 4 fasi, eseguite secondo la tecnica di pipelining:

Fetch
Indirizzamento della memoria in lettura col registro Program Counter in modalità autoincremento (accesso alla cella di indirizzo specificato dal contenuto del registro PC, e incremento di 4 del valore contenuto in PC);
Decodifica
riconoscimento del formato e del codice operativo dell'istruzione della quale é stato completato il fetch nel ciclo di clock precedente; Nel caso di istruzioni di salto non condizionale (Branch) o di chiamata o di ritorno da procedura (Call e JMPL) viene anche alterato il valore del registro Program Counter usato dall'unità di Fetch;
Esecuzione interna
Esecuzione di qualsiasi istruzione decodificata nel ciclo di clock precedente che non richieda l'accesso alla memoria (tutte tranne Load e Store). Nel caso di istruzioni di salto condizionale di programma (Branch legato ai bit di condizione icc) viene anche alterato il valore del registro Program Counter usato dall'unità di Fetch;
Completamento dell'accesso alla memoria
Fase usata solo per le istruzioni Load e Store per completare l'accesso alla memoria. Nessuna delle altre istruzioni utilizza questo quarto modulo della pipeline.
L'uso di tale struttura permette in generale l'esecuzione di un programma strettamente sequenziale (ossia con istruzioni allocate in celle di memoria di indirizzo successivo) con una velocità di una istruzione completata per ogni ciclo di clock. Questo accade dopo l'inizio dell'esecuzione delle prime tre istruzioni sequenziali nei primi tre cicli di clock, ossia dopo che la pipeline é stata completamente "caricata".

Tale modalità di esecuzione non si presta bene invece per l'esecuzione di istruzioni di salto di programma nello stile tipico delle macchine convenzionali di tipo Von Neumann. Infatti una istruzione di salto di programma viene riconosciuta come tale solo alla fine della fase di Decodifica, dopo che l'unità di Fetch ha già avviato il Fetch dalla cella di memoria di indirizzo immediatamente successivo a quello della cella contenente il codice della istruzione di salto. Simile problema si manifesta anche per le istruzioni di chiamata e ritorno da procedura.

Per non ridurre eccessivamente l'efficienza del pipeline durante l'esecuzione di queste istruzioni di cambiamento del valore del registro PC, l'architettura SPARC prevede l'introduzione di un "ritardo prefissato" nella esecuzione di queste istruzioni che alterano il contenuto del registro PC detto Delay Slot. Tale tecnica consiste nel "definire come corretto" il comportamento insito nell'adozione del pipeline, ossia il fatto che l'alterazione del valore contenuto nel registro PC rispetto all'incremento sequenziale debba aver effetto solo dopo l'esecuzione dell'istruzione codificata nella cella di memoria di indirizzo successivo a quella che contiene il codice della istruzione di salto (o di chiamata/ritorno da procedura). La definizione di un delay slot permette di correggere completamente la situazione nel caso di istruzioni di salto non condizionale, di chiamata e di ritorno da procedura. La correzione é parziale nel caso di istruzioni di salto condizionale (in quanto la correzione completa richiederebbe l'introduzione di 2 delay slots in quest'ultimo caso).

Partendo da una struttura di programma organizzata pensando di usare le "solite istruzioni di salto" alla Von Neumann (con effetto immediato) si puo' passare ad un programma equivalente che usi invece i "salti con delay slot" invertendo l'ordine di allocazione in memoria della istruzione di salto con l'istruzione immediatamente precedente. Nel caso in cui non sia possibile operare tale inversione (per esempio perche' il salto non e' logicamente preceduto da nessuna altra istruzione che non sia a sua volta una istruzione di salto) la soluzione consiste nell'aggiungere dopo l'istruzione di salto una "pseudoistruzione" (indicata col simbolo NOP ) che non altera il valore di nessun registro ne locazione di memoria, e che ha come unico scopo quello di lasciar trascorrere inutilmente il tempo corrispondente ad un ciclo di clock. L'individuazione di una "istruzione utile" da piazzare nel Delay Slot di una istruzione di salto al posto della pseudoistruzione NOP é una delle tecniche di ottimizzazione che possono essere adottate per migliorare il tempo di esecuzione di un programma nel caso di una architettura che usa la tecnica di pipelining come SPARC.

Un'altra forma di interazione tra stadi diversi della pipeline che può avere effetti negativi sul tempo di esecuzione delle istruzioni deriva dalla esecuzione simultanea della fase 4 (completamento dell'accesso alla memoria) di una istruzione e della fase 3 (esecuzione interna) della istruzione successiva. In particolare, se la fase 4 viene usata per il completamento di una istruzione di tipo Load (lettura della memoria ed inserzione del nuovo valore in un registro), il valore sarà effettivamente disponibile nel registro solo al termine del ciclo di clock. La fase 3 della istruzione successiva non deve quindi far riferimento al contenuto di tale registro per poter essere eseguita simultaneamente. Situazioni "anomale" (uso nella istruzione immediatamente successiva del registro che si sta finendo di caricare) vengono riconosciute automaticamente dalla microarchitettura SPARC, e comportano il ritardo di un intero ciclo di clock per l'attivazione della fase 3 della istruzione successiva. Tale meccanismo (detto Hardware Interlocking) ritarda l'esecuzione di tutte le istruzioni successive alla Load di un ciclo di clock, provocando quindi un rallentamento della esecuzione del programma.

Tornando alla definizione delle istruzioni di salto condizionale, queste fanno riferimento ai valori assunti da un insieme di 4 bit (contenuti nel registo di stato dell'unità degli interi). L'insieme viene indicato col simbolo icc (Integer Condition Code), ed é costituito dai 4 bit chiamati N, Z, V, C:

N
rappresenta la condizione "numero negativo" (in rappresentazione complemento a 2): coincide col bit di segno, ossia vale 1 per numeri negativi e 0 per numeri positivi;
Z
rappresenta la condizione "numero uguale a zero" (in rappresentazione complemento a 2): corrisponde all'applicazione della funzione NOR tra tutti i bit della rappresentazione di un numero, ossia vale 1 quando tutti i bit della rappresentazione di un numero su 32 bit valgono 0, e 0 altrimenti;
V
rappresenta la condizione "overflow" per la rappresentazione di un numero in complemento a 2: assume il valore 1 quando l'overflow di un numero positivo produce un valore negativo o viceversa quando l'overflow di un numero negativo produce un valore positivo; assume il valore 0 quando non si verifica overflow;
C
rappresenta la condizione "riporto" (in rappresentazione binaria per numeri naturali): corrisponde al 33esimo bit di una rappresentazione binaria senza segno.
Come vedremo piu' avanti i bit di condizione possono essere alterati o meno mediante l'esecuzione di istruzioni aritmetico-logiche, in dipendenza del valore calcolato ed inserito nel registro destinazione.

Insieme delle istruzioni e formati di codifica

Le istruzioni SPARC vengono tutte codificate su 32 bit, in modo da poter utilizzare una singola cella di memoria per contenere la rappresentazione di una istruzione. Viene utilizzata una forma particolare della tecnica di espansione del codice operativo delle istruzioni che comporta l'individuazione di 5 formati diversi per la rappresentazione delle istruzioni. Di questi 5 formati, 3 sono univocamente legati alla rappresentazione di altrettante istruzioni macchina (individuate dagli acronimi SETHI, BRANCH e CALL). I rimanenti due formati sono usati per la rappresentazione di tutte le altre istruzioni macchina; la scelta del formato da usare é legata in questo caso all'uso di due diversi modi di indirizzamento per l'individuazione del secondo operando: il modo Registro oppure il modo Immediato.

1a
formato per indirizzamento Registro del secondo operando
      VU TSRQP ONMLKJ IHGFE D CBA98765 43210
      00 dest  opcode src-1 0 fp-op    src-2
    
da sinistra verso destra, troviamo un primo campo composto dai due bit al valore costante "00" che identificano il "formato 1", il campo "dest" rappresentato su 5 bit, il campo "opcode" su 6 bit, il campo "src-1" su 5 bit, il bit al valore costante "0" che identifica il "sottoformato a" (indirizzamento Registro), il campo "fp-op" su 8 bit ed il campo "src-2" su 5 bit.
1b
formato per indirizzamento Immediato del secondo operando
      VU TSRQP ONMLKJ IHGFE D CBA9876543210
      00 dest  opcode src-1 1 constant
    
da sinistra verso destra, troviamo un primo campo composto dai due bit al valore costante "00" che identificano il "formato 1", il campo "dest" rappresentato su 5 bit, il campo "opcode" su 6 bit, il campo "src-1" su 5 bit, il bit al valore costante "1" che identifica il "sottoformato b" (indirizzamento Immediato) ed il campo "constant" su 13 bit.
2
formato per rappresentazione dell'istruzione SETHI
      VU TSRQP ONM LKJIHGFEDCBA9876543210
      01 dest  op  constant
    
da sinistra verso destra, troviamo un primo campo composto dai due bit al valore costante "01" che identificano il "formato 2" e l'istruzione SETHI, il campo "dest" rappresentato su 5 bit, il campo "op" su 3 bit ed il campo "constant" su 22 bit.
3
formato per rappresentazione dell'istruzione BRANCH
      VU T SRQP ONM LKJIHGFEDCBA9876543210
      10 a cond op  pc-rel-displacement
    
da sinistra verso destra, troviamo un primo campo composto dai due bit al valore costante "10" che identificano il "formato 3" e l'istruzione BRANCH, il campo "a" rappresentato su 1 bit, il campo "cond" su 4 bit, il campo "op" su 3 bit ed il campo "pc-rel-displacement" su 22 bit.
4
formato per rappresentazione dell'istruzione CALL
      VU TSRQPONMLKJIHGFEDCBA9876543210
      11 pc-rel-displacement
    
da sinistra verso destra, troviamo un primo campo composto dai due bit al valore costante "11" che identificano il "formato 4" e l'istruzione CALL ed il campo "pc-rel-displacement" su 30 bit.

Istruzioni di tipo Load

Le istruzioni di tipo Load sono differenziate a seconda della rappresentazione binaria dell'operando "sorgente":

LDSB
Load Signed Byte. Caricamento nel registro da 32 bit identificato dal campo "dest" del contenuto di 8 bit di RAM con estensione del segno in complemento a 2.
LDSH
Load Signed Halfword. Caricamento nel registro da 32 bit identificato dal campo "dest" del contenuto di 16 bit di RAM con estensione del segno in complemento a 2.
LDUB
Load Unsigned Byte. Caricamento nel registro da 32 bit identificato dal campo "dest" del contenuto di 8 bit di RAM con azzeramento dei 24 bit più significativi.
LDUH
Load Unsigned Halfword. Caricamento nel registro da 32 bit identificato dal campo "dest" del contenuto di 16 bit di RAM con azzeramento dei 16 bit più significativi.
LD
Load Word. Caricamento nel registro da 32 bit identificato dal campo "dest" del contenuto di 32 bit di RAM.
LDD
Load Doubleword. Caricamento nei due registri consecutivi da 32 bit identificati dai 4 bit più significativi del campo "dest" del contenuto di 64 bit di RAM.

In ogni caso la memoria viene indirizzata producendo un valore pari alla somma dei valori dei due operandi "src-1" e "src-2" identificati nel formato di rappresentazione dell'istruzione. Come tutte le altre istruzioni SPARC diverse da SETHI, BRANCH e CALL, viene adottato il formato 1.a oppure 1.b: nel caso 1.a src-1 ed src-2 sono due registri dell'unità IU, e l'indirizzo della cella di memoria a cui far riferimento viene calcolato sommando il contenuto dei due registri; nel caso 1.b, "src-2" é una costante specificata in modo immediato ed "src-1" un registro, quindi viene realizzato il modo di indirizzamento "indicizzato rispetto al registro src-1".

In ogni caso l'indirizzo ottenuto come somma degli operandi "src-1" ed "src-2" deve soddisfare dei vincoli di allineamento dipendenti dalla dimensione dei dati movimentati. L'indirizzo di una "doubleword" (associato alla istruzione LDD) viene espresso su 32 bit, ma deve necessariamente contenere i valori "000" nelle tre cifre meno significative, in modo da rappresentare un numero multiplo di 2**3. L'indirizzo di una "word" (associato alla istruzione LD) viene espresso su 32 bit, ma deve necessariamente contenere i valori "00" nelle due cifre meno significative, in modo da rappresentare un numero multiplo di 2**2. L'indirizzo di una "halfword" (associato alle istruzioni LDSH e LDUH) viene espresso su 32 bit, ma deve necessariamente contenere il valore "0" nella cifra meno significativa, in modo da rappresentare un numero pari. L'indirizzo di un "byte" (associato alle istruzioni LDSB e LDUB) viene espresso su 32 bit, senza vincoli di allineamento (in quanto rappresenta la minima entità indirizzabile in memoria).

In alcuni casi occorre poi tener presente che l'architettura SPARC adotta la convenzione detta Big Endians per la numerazione dei bytes all'interno di Halfword, Word e Doubleword. Questo significa che i byte vengono numerati a partire da quelli che contengono i bit di order superiore (il byte 0 all'interno di una word é quello che contiene il bit di segno, mentre il byte 3 all'interno di una word é quello che contiene le cifre meno significative della rappresentazione). Tale convenzione di numerazione "da sinistra verso destra" può essere contrapposta alla convenzione inversa adottata per esempio nelle architetture Intel della famiglia "x86" detta Little Endians, secondo la quale i byte vengono numerati "da destra verso sinistra", e dove quindi per esempio il byte numero 0 corrisponde con gli 8 bit meno significativi di una Word ed il byte 3 contiene il bit di segno di una rappresentazione in complemento a due su 32 bit.

Notare infine la relazione tra semantica dell'istruzione e struttura pipeline per la sua esecuzione. La produzione dell'indirizzo della cella di memoria ottenuto sommando gli operandi "src-1" ed "src-2" avviene nella terza fase della pipeline (esecuzione interna), mentre l'accesso vero e proprio alla memoria usando tale indirizzo viene realizzato nella fase 4 (completamento accesso alla memoria). Il valore viene quindi inserito nel registro "dest" solo al termine della fase 4, e non risulta essere disponibile come sorgente per l'esecuzione nella fase 3 della istruzione successiva. Tale problema viene gestito mediante un meccanismo di Interlocking a livello hardware, che provoca l'inserzione di un ciclo di ritardo per le istruzioni successive in caso l'istruzione successiva individui lo stesso registro specificato come "dest" della Load anche come "src-1" oppure come "src-2" per l'istruzione immediatamente successiva.

Istruzioni di tipo Store

Le istruzioni di tipo Store sono differenziate a seconda della rappresentazione binaria dell'operando:

STB
Store Byte. Scrittura in 8 bit di RAM degli 8 bit meno significativi della rappresentazione contenuta nel registro a 32 bit indicato nel campo "dest".
STH
Store Halfword. Scrittura in 16 bit di RAM dei 16 bit meno significativi della rappresentazione contenuta nel registro a 32 bit indicato nel campo "dest".
ST
Store Word. Scrittura in 32 bit di RAM dei 32 bit contenuti nel registro indicato nel campo "dest".
STD
Store Doubleword. Scrittura in 64 bit di RAM del contenuto dei due registri consecutivi da 32 bit identificati dai 4 bit più significativi del campo "dest".

Così come nel caso delle istruzioni Load, l'indirizzo della memoria viene calcolato sommando i valori "src-1" ed "src-2". L'indirizzo così calcolato é soggetto agli stessi vincoli di allineamento illustrati per le istruzioni Load. Nel caso il registro "dest" contenga una rappresentazione su un numero di bit superiore, il valore viene semplicemente "troncato" inserendo in memoria il numero specificato (8 per STB o 16 per STH) di bit meno significativi senza segnalare alcuna condizione di errore.

Istruzioni di tipo Aritmetico

Le istruzioni di tipo Aritmetico sono differenziate a seconda della manipolazione effettuata sui bit di rappresentazione dello stato:

ADD
Add without changing icc.
ADDCC
Add, set icc.
ADDX
Add with carry without changing icc.
ADDXCC
Add with carry, set icc.
SUB
Subtract without changing icc.
SUBCC
Subtract, set icc.
SUBX
Subtract with carry without changing icc.
SUBXCC
Subtract with carry, set icc.

La versione base delle istruzioni ADD e SUB calcola il valore "src-1" +/- "src-2" e memorizza il risultato nel registro "dest" senza alterare ne prendere in considerazione i bit di condizione. La versione rappresentata dal nome col suffisso "CC" calcola lo stesso valore, ma determina anche un cambiamento dei bit N, Z, V, C del registro di stato, consistente col valore calcolato. Le istruzioni ADDX e SUBX calcolano inve il valore "src-1" +/- "src-2" + C (ossia sommano anche il valore del bit "riporto" contenuto in icc).

Il processore SPARC prevede altre istruzioni aritmetiche più complesse e di uso meno frequente che non staremo ad indagare (per esempio MULSCC, passo di moltiplicazione mediante algoritmo di somma e scorrimento).

Istruzioni di tipo Logico

Le istruzioni di tipo Logico sono differenziate a seconda della manipolazione effettuata sui bit di rappresentazione dello stato:

AND
Boolean AND without changing icc.
ANDCC
Boolean AND, set icc.
ANDN
Boolean NAND without changing icc.
ANDNCC
Boolean NAND, set icc.
OR
Boolean OR without changing icc.
ORCC
Boolean OR, set icc.
ORN
Boolean NOR without changing icc.
ORNCC
Boolean NOR, set icc.
XOR
Boolean exclusive OR without changing icc.
XORCC
Boolean exclusive OR, set icc.
XNOR
Boolean exclusive NOR without changing icc.
XNORCC
Boolean exclusive NOR, set icc.

Realizzano le funzioni logiche AND, NAND, OR, NOR, XOR e XNOR, nelle due versione con e senza alterazione dei bit icc nel registro di stato, secondo lo stesso schema a tre operandi delle istruzioni aritmetiche, ossia calcolo di "src-1" operatore-logico "src-2", ed inserzione del risultato ottenuto nel registro "dest".

Istruzioni di Scorrimento

Sono previste due operazioni di scorrimento generalizzato a destra ed una a sinistra:

SLL
Shift Left Logical
SRL
Shift Right Logical
SRA
Shift Right Arithmetic

In ogni caso l'operando "src-1" rappresenta la configurazione binaria da far scorrere a destra o a sinistra, mentre il valore dell'operando "src-2" indica il numero di posizioni dello scorrimento; il risultato dello scorrimento viene memorizzato nel registro "dest".

La differenza tra le istruzioni SRL ed SRA consiste nella interpretazione (e quindi nella manipolazione) della rappresentazione come numero con segno in complemento a due o meno. SRL aggiunge sempre bit al valore 0 nelle posizioni più a sinistra per completare la rappresentazione a 32 bit dopo l'operazione di scorrimento di n posizioni che ha eliminato per l'appunto n bit. In tal modo l'operazione é consistente con una divisione per una potenza di due pensando ad una rappresentazione binaria di numeri senza segno. SRA invece ripete il 32esimo bit della rappresentazione originale "src-1" per riempire gli n bit mancanti dopo lo scorrimento a destra di n posizioni, mantenendo quindi lo stesso segno del numero originale. In tal modo l'operazione é consistente con una divisione per una potenza di due pensando ad una rappresentazione in complemento a due.

Istruzioni di tipo Tagged

... da scrivere ...

Istruzioni di controllo di flusso

Esaminiamo ora le istruzioni di salto di programma (condizionale e non), chiamata e ritorno di procedura, attivazione e terminazione di trap e scorrimento della finestra di visibilità dei registri

Bxx
(Conditional) Branch. Usa il formato di codifica 3. Le informazioni codificate nei campi "cond" (4 bit) e "op" (3 bit) concorrono a stabilire la condizione che deve essere soddisfatta per operare il salto.

Una delle opzioni (indicata dall'uso del simbolo "B") e' la condizione "sempre vero", che realizza pertanto il salto non condizionale. Altre possibilità includono il test di uno o più dei 4 bit icc (secondo la maschera stabilita dal campo "cond"), calcolando la funzione booleana stabilita dal campo "op". Tali possibilità vengono indicate simbolicamente sostituendo "xx" nel nome dell'istruzione con i caratteri "GT" (Greater Than 0), "GE" (Greater than or Equal to 0), "EQ" (EQual to 0), "NE" (Not Equal to 0), "LE" (Less than or Equal to 0), "LT" (Less Than 0), ecc.

L'esecuzione dell'istruzione Branch comporta la sostituzione del valore contenuto nel registro Program Counter con un nuovo valore, ottenuto sommando al valore precedente del registro PC la costante rappresentata su 22 bit nel campo "pc-rel-displacement" del formato 3 moltiplicata per 4. Tale costante viene interpretata come un numero con segno in rappresentazione complemento a 2, quindi la somma viene realizzata dopo aver esteso la rappresentazione della costante su 32 bit con estensione del segno. In questo modo é possibile sia incrementare che decrementare il valore contenuto nel registro PC in un intervallo di valori di ampiezza predeterminata (da -2**23 a +2**23-1).

Il campo "A" (un bit) permette di specificare l'annullamento dell'esecuzione dell'istruzione contenuta nel Delay Slot del Branch nel caso in cui la condizione di salto non sia verificata. Campo A uguale ad 1 (rappresentato simbolicamente aggiungendo il suffisso ",A" al nome dell'istruzione) attiva l'opzione di annullamento del delay slot nel caso di condizione di salto non soddisfatta. Campo A uguale a 0 (rappresentato simbolicamente evitando di aggiungere il suffisso ",A" al nome dell'istruzione) definisce invece che l'istruzione contenuta nel delay slot deve essere eseguita in ogni caso, indipendentemente dal fatto che la condizione di salto sia soddisfatta o meno.

Txx
(Conditional) Trap: interruzione del programma in esecuzione, attivazione del Trap Handler predefinito nel vettore di interruzione e contemporaneo passaggio al livello di privilegio "kernel" per l'esecuzione di istruzioni privilegiate.

L'attivazione della trap può essere non condizionale oppure condizionata al valore di verità di una condizione sul contenuto dei bit dell'insieme icc rappresentata nello stesso modo adottato per l'istruzione Branch.

CALL
Chiamata di procedura di indirizzo predefinito. Usa il formato di rappresentazione 4. L'esecuzione di questa istruzione prevede la copia del valore contenuto in PC nel registro O7, e quindi la sua sostituzione col valore ottenuto sommando al valore precedente di PC il valore costante specificato nei 30 bit del campo "pc-rel-displacement" moltiplicato per 4. Notare che tale operazione di somma può determinare una condizione di overflow che viene semplicemente ignorata (e che può essere sfruttata per produrre valori inferiori al valore precedente del registro PC. In tal modo, si può pertanto generare l'indirizzo di qualsiasi parola di memoria nello spazio di indirizzamento virtuale su 32 bit del processore a partire da qualunque altro valore del registro PC.
JMPL
"Jump and Link": istruzione generalizzata di chiamata e ritorno da procedura con indirizzo contenuto in registro.

Questa istruzione usa i formati di rappresentazione 1.a o 1.b. I due operandi sorgente vengono sommati in modo da produrre un nuovo valore da inserire nel registro PC. L'operando "dest" specifica il registro nel quale viene salvato il valore precedente del registro PC prima di ricoprirlo col nuovo valore calcolato sommando "src-1" + "src-2". Questo comportamento può essere utilizzato per ottenere le seguenti "pseudoistruzioni":

SAVE
Scorrimento verso l'alto della finestra mobile di visibilità sul register file (a seguito dell'attivazione di una procedura). Anche questa istruzione usa il formato 1 di codifica, e simultaneamente allo scorrimento della finestra di visibilità usa l'indicazione dei tre operandi "dest", "src-1" ed "src-2" per realizzare una istruzione ADD. Tale opportunità viene normalmente sfruttata per inizializzare il registro Stack Pointer della nuova finestra di visibilità sottraendo un valore predeterminato al valore del registro SP della posizione vecchia della finestra (che si appresta a diventare il registro FP della nuova finestra). Questo risultato viene ottenuto specificando SP come operando "dest" e come operando "src-1", ed un valore costante negativo come operando "src-2" nel formato 1.b. Notare che il registro specificato come "src-1" fa riferimento alla posizione di finestra precedente allo scorrimento, mentre il registro specificato come "dest" fa riferimento alla posizione di finestra successiva allo scorrimento.
RESTORE
Scorrimento verso il basso della finestra mobile di visibilità sul register file (a seguito del ritorno da una procedura al programma chiamante).
RETT
"Return form Trap": terminazione del Trap Handler con ripristino del programma precedentemente interrotto e ritorno al livello di privilegio "user".

Altre Istruzioni

L'architettura SPARC prevede un certo numero di altre istruzioni, di uso meno comune e/o "privilegiate" (ossia eseguibili solo col livello di privilegio "kernel"), che tralasceremo in questi appunti. Ci limitiamo a riportare l'istruzione di uso più comune:

SETHI
"Set Higher bits": usa il modo di indirizzamento immediato per specificare il nuovo valore da inserire in un registro. Usa il formato di rappresentazione 2, nel quale il campo "constant" codifica i 22 bit più significativi della rappresentazione su 32 bit da inserire nel registro individuato dal campo "dest". I 10 bit meno significativi del registro vengono normalmente riportati a 0 (anche se a livello di architettura é prevista la possibilità di calcolarne il nuovo valore in funzione del valore precedente applicando la funzione specificata nel campo "op").



Assembler SPARC

... da scrivere ...

Struttura dei programmi

... da scrivere ...

Esempi di programmi

Cominciamo a considerare un semplicissimo esempio di funzione che scandisce gli elementi di un vettore di interi con lo scopo di calcolarne la somma ed il valore massimo. Diamo una specifica di tale programma usando un linguaggio ad alto livello simile al VHDL usato per le esercitazioni di laboratorio (notare che questo programma non rispetta completamente la sintassi VHDL a causa della presenza di un parametro "out" per la funzione):

  constant N = 9;
  type vettore is array (0 to N) of integer;
  variable vet : vettore;
  variable som, max : integer;

  function sommavet( variable mm : out integer;
                     variable vv : in vettore;
                     variable nn : in integer )
          return integer is
        variable ss : integer := 0;
        begin
          for ii in 0 to nn loop
              if ( ii = 0 ) or ( mm <= vv(ii) ) then
                  mm := vv(ii);
                endif;
              ss := ss + vv(ii);
            endloop;
          return ss
        end;

...

  som := sommavet(max, vet, N);

...
Tale funzione potrebbe essere tradotta in Assembler per l'architettura SPARC come segue (il codice seguente é stato ottenuto utilizzando il compilatore GNU CC 2.7.0 a partire da una equivalente specifica della funzione in linguaggio C):
sommavet: mov %o0, %o4 ! sposta indirizzo di "mm" in %o4
mov 0, %o0 ! inizializza "ss" in %o0
cmp %g0, %o2
bge .LL11:
mov 0, %o3 ! inizializza "ii" in %o3
sll %o2, 2, %o2 ! moltiplica per 4 "nn" in %o2
.LL13: cmp %o3, 0 ! traduzione di "if (ii = 0)"
be .LL15 ! salta al ramo "then"
ld [%o3+%o1], %g3 ! delay slot: carica "vv(ii)" in %g3
ld [%o4], %g2 ! carica "mm" in %g2
cmp %g3, %g2 ! traduzione di "if (mm <= vv(ii)"
ble,a .LL17 ! salta ad "endif"
add %o3, 4, %o3 ! delay slot con annullamento: incrementa "ii"
.LL15: st %g3, [%o4] ! traduzione di "mm := vv(ii)"
add %o3, 4, %o3 ! incrementa "ii"
.LL17: cmp %o3, %o2 ! verifica la condizione di uscita dal "loop"
bl .LL13 ! torna all'inizio del "loop" col nuovo valore di "ii"
add %o0, %g3, %o0 ! delay slot: traduzione di "ss := ss + vv(ii)"
.LL11 retl ! ritorna al programma chiamante
nop ! delay slot vuoto

Il parametro di uscita "max" in questo caso é stato allocato in memoria, per cui la funzione deve avere l'indirizzo della cella di memoria corrispondente alla variabile "max" in modo da poter cambiarne il contenuto e produrre quindi in uscita un valore diverso da quello assunto al momento dell'attivazione della funzione. Il fatto di associare al registro usato per individuare il parametro "mm" l'indirizzo della cella di memoria nella quale é allocata "max" viene detto "passaggio di parametro per referenza". Il passaggio del parametro potrebbe essere notevolmente semplificato se la variabile "max" fosse stata dichiarata come variabile locale al programma che richiama la funzione e quindi allocata in un registro dell'unità degli interi anzichè in una parola di memoria. In tal caso il registro usato per il parametro "mm" avrebbe potuto contenere direttamente il valore della variabile "max".

Notare che il passaggio del parametro N (massimo indice del vettore) come variabile di tipo intero pone il problema di prevedere anche il caso di "nn" negativo (riconosciuto attraverso l'esecuzione della terza e quarta istruzione (CMP seguita da BGE .LL11). Vediamo invece come potrebbe essere semplificato il programma se il valore della costante N=9 fosse noto anche all'interno della funzione come costante (oltre a passare il parametro "max" direttamente in un registro):
sommavet: mov 0, %o0 ! inizializza "ss" in %o0
mov 0, %o3 ! inizializza "ii" in %o3
.LL13: cmp %o3, 0 ! traduzione di "if ( ii = 0 )"
be .LL15 ! passa al ramo "then"
ld [%o3+%o1], %g3 ! delay slot: carica "vv(ii)" in %g3
cmp g3, %o2 ! traduzione di "if ( mm <= vv(ii)"
bgt,a .LL17 ! annullamento del delay slot se la condizione precedente é falsa
.LL15 mov %g3, %o2 ! delay slot e ramo "then" : traduzione di "mm := vv(ii)"
.LL17 add %o3, 4, %o3 ! "endif" : passa al valore successivo di "ii"
cmp %o3, 36 ! verifica la condizione di uscita dal "loop"
ble .LL13 ! ritorna all'inizio del "loop" e ripeti col nuovo valore di "ii"
add %o0, %g3, %o0 ! delay slot: traduzione di "ss := ss + vv(ii)"
retl ! torna al programma chiamante
nop ! delay slot vuoto

Altri esempi di programmi

... da scrivere ...