Vai al contenuto
Sistemi di Elaborazione

Il set di istruzioni: RISC e CISC, RISC-V e assembly

RISC e CISC, registri di RISC-V, codifica delle istruzioni con esempio in bit, modi di indirizzamento, un programma in C e in assembly, chiamate di funzione e stack.

Il set di istruzioni (ISA, Instruction Set Architecture) è il contratto tra il software e l’hardware: l’elenco delle istruzioni che un processore sa eseguire, come sono codificate in bit, quali registri ci sono e come si indirizza la memoria. Un programma compilato per un’ISA gira su qualsiasi processore che la realizza, dal più economico al più veloce. In questa lezione usiamo come esempio RISC-V, un’ISA aperta e moderna, e la confrontiamo con le architetture più complesse.

RISC e CISC

  • Le architetture CISC (Complex Instruction Set Computer), come la x86 dei PC, hanno istruzioni molto varie, di lunghezza variabile, che possono operare direttamente sulla memoria e fare molte cose insieme.
  • Le architetture RISC (Reduced Instruction Set Computer), come ARM e RISC-V, hanno poche istruzioni semplici e regolari, tutte della stessa lunghezza; solo le istruzioni di caricamento (load) e memorizzazione (store) accedono alla memoria, tutti i calcoli avvengono tra registri.

Le istruzioni RISC sono più numerose a parità di programma, ma ciascuna è più semplice da decodificare e da eseguire in pipeline: N_{istr} cresce, il CPI e il periodo di clock calano. Oggi la distinzione è sfumata: i processori x86 traducono internamente le istruzioni complesse in micro-operazioni di tipo RISC, e i telefoni e molti computer portatili usano processori ARM.

I registri di RISC-V

RISC-V a 32 bit (RV32I) ha 32 registri da 32 bit, da x0 a x31. Il registro x0 vale sempre zero, e scriverci non ha effetto: sembra uno spreco, ma permette di ottenere molte operazioni senza istruzioni dedicate (copiare un registro è sommargli zero). Per convenzione gli altri hanno ruoli precisi: ra per l’indirizzo di ritorno, sp per il puntatore allo stack, a0–a7 per gli argomenti delle funzioni, t0–t6 per i valori temporanei, s0–s11 per quelli da preservare.

La codifica delle istruzioni

Tutte le istruzioni sono lunghe 32 bit e hanno pochi formati, con i campi negli stessi posti, così la decodifica è rapida. Il formato R serve per le operazioni tra registri:

Formato R delle istruzioni RISC-V con la codifica di add x5, x6, x7
I 32 bit dell’istruzione sono divisi nei campi funct7 (bit 31-25), rs2 (24-20), rs1 (19-15), funct3 (14-12), rd (11-7) e opcode (6-0). Per add x5, x6, x7 i campi valgono 0, 7, 6, 0, 5 e 0110011, e l’istruzione in esadecimale è 0x007302B3.

Esempio. add x5, x6, x7 (cioè x_5 = x_6 + x_7): opcode 0110011 (operazione tra registri), rd = 5, rs1 = 6, rs2 = 7, funct3 e funct7 a zero (somma, non sottrazione). Mettendo in fila i bit si ottiene la parola 0x007302B3: è questo numero che il processore trova in memoria.

Gli altri formati: I per le operazioni con una costante e i caricamenti (12 bit di costante al posto di rs2 e funct7), S per le memorizzazioni, B per i salti condizionati, U e J per le costanti grandi e i salti lunghi.

I modi di indirizzamento

RISC-V ne ha pochissimi:

  • immediato: la costante sta nell’istruzione, addi t0, t0, 4;
  • registro: add a2, a2, t0;
  • base più spiazzamento: l’indirizzo è un registro più una costante, lw t0, 8(a0) legge la parola all’indirizzo a_0 + 8. Basta per accedere agli elementi di un vettore e ai campi di una struttura;
  • relativo al PC: i salti indicano di quanto spostarsi rispetto all’istruzione attuale, così il codice funziona in qualsiasi posizione della memoria.

Le architetture CISC aggiungono modi più ricchi (indiretto, indicizzato con scala, autoincremento), che risparmiano istruzioni ma complicano l’hardware.

Un esempio di programma

La somma degli elementi di un vettore di interi. In C:

int somma(int *v, int n) {
    int s = 0;
    for (int i = 0; i < n; i++)
        s += v[i];
    return s;
}

In assembly RISC-V, con a0 = indirizzo del vettore, a1 = numero di elementi, risultato in a0:

somma:  li    t1, 0          # s = 0
ciclo:  beqz  a1, fine       # se n == 0 esci
        lw    t0, 0(a0)      # t0 = *v
        add   t1, t1, t0     # s += t0
        addi  a0, a0, 4      # v++ (un int occupa 4 byte)
        addi  a1, a1, -1     # n--
        j     ciclo
fine:   mv    a0, t1         # valore restituito
        ret

Si vede il lavoro del compilatore: l’indice è sparito, sostituito da un puntatore che avanza di 4 byte; il ciclo usa cinque istruzioni per elemento. Ogni istruzione fa una cosa sola, e proprio per questo il processore può eseguirne diverse in sovrapposizione.

Le chiamate di funzione e lo stack

jal ra, f salta alla funzione f salvando in ra l’indirizzo di ritorno; ret (cioè jalr x0, 0(ra)) torna indietro. Se una funzione ne chiama un’altra, deve salvare ra e i registri s che usa in una zona di memoria gestita a pila, lo stack: si fa spazio sottraendo a sp e si ripristina alla fine. Le regole su quali registri preservare formano la convenzione di chiamata, che permette a funzioni compilate separatamente, anche in linguaggi diversi, di chiamarsi tra loro.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x