Matrici: due indici, un indirizzo

Matrici: due indici, un indirizzo

Una matrice è una griglia di numeri disposti in righe e colonne. Una riga è una linea orizzontale della griglia, letta da sinistra a destra. Una colonna è una linea verticale della griglia, letta dall'alto verso il basso. La memoria non ha alcun concetto di righe o colonne. È una lunga sequenza di byte, lo stesso tipo di vettore trattato nella lezione precedente. Per memorizzare una matrice, le sue righe e colonne devono essere collocate una dopo l'altra in quell'unica sequenza. La scelta usuale è l'ordine row-major: memorizzare ogni numero della prima riga, poi ogni numero della seconda riga, e continuare finché ogni riga è stata collocata.

Due indici, un unico indirizzo

La posizione di un elemento in una matrice è data da due indici: l'indice di riga, a quale riga appartiene, e l'indice di colonna, a quale colonna appartiene. Il conteggio per entrambi inizia da 0, allo stesso modo in cui funzionavano gli indici nella lezione sui vettori. La memoria contiene un'unica sequenza piatta di numeri, quindi i due indici devono prima essere combinati in un unico indice appiattito: moltiplicare l'indice di riga per il numero di colonne della matrice, poi aggiungere l'indice di colonna. Da lì l'indice appiattito diventa un indirizzo di byte allo stesso modo dell'indice di un vettore: address = base + index * 4.

Appiattimento di una riga e di una colonna in un indirizzo
mul t4, t1, t3 # t4 = indice di riga * numero di colonne
add t4, t4, t2 # t4 = quello + indice di colonna (l'indice appiattito)
slli t4, t4, 2 # t4 = offset in byte = indice * 4
add t5, t0, t4 # t5 = indirizzo dell'elemento a questa riga e colonna
lw t5, 0(t5) # t5 = valore a quell'indirizzo

moltiplica l'indice di riga per il numero di colonne. Nella lezione sui vettori la moltiplicazione poteva essere fatta con uno scorrimento perché 4 è una potenza di due, ma il numero di colonne di una matrice di solito non lo è, quindi qui viene usato invece di . L'indice di colonna viene aggiunto, e il risultato viene scalato per 4 con un altro (4 è una potenza di due, quindi uno scorrimento funziona ancora per questa parte). Questo produce l'offset in byte dell'elemento alla riga e colonna date.

Indicizzazione per riga principale
index=0× 3 +0=0
1
0x10010000
2
0x10010004
3
0x10010008
4
0x1001000c
5
0x10010010
6
0x10010014
7
0x10010018
8
0x1001001c
9
0x10010020
01
0x10010000
12
0x10010004
23
0x10010008
34
0x1001000c
45
0x10010010
56
0x10010014
67
0x10010018
78
0x1001001c
89
0x10010020

Riga 0, colonna 0 corrisponde all'indice 0.

La griglia a sinistra mostra come una matrice viene disegnata su carta. L'unica riga a destra mostra come quegli stessi numeri sono disposti in memoria. L'evidenziazione si muove attraverso la griglia in ordine di lettura: riga 0 da sinistra a destra, poi riga 1, poi riga 2. Nello stesso momento lo stesso elemento si illumina nella riga appiattita sottostante, perché moltiplicare l'indice di riga per il numero di colonne e aggiungere l'indice di colonna dà l'indirizzo di quell'elemento, che corrisponde al valore esadecimale mostrato su entrambi i lati. RISC-V lavora solo con la riga appiattita a destra. La griglia è una forma che il layout .data del tuo programma e la tua stessa lettura di esso impongono a quell'unica riga, usando l'aritmetica , e mostrata sopra. Una volta che la griglia e la riga appiattita sono comprese come una sola cosa mostrata due volte, lo stesso schema copre qualsiasi griglia di dati: le righe di pixel di un'immagine, un tabellone di gioco o un foglio di calcolo. Ognuno è un array, raggiunto con una moltiplicazione in più.

Somma di una matrice

Somma ogni elemento di una matrice 3 per 3
.data
m: .word 1, 2, 3, 4, 5, 6, 7, 8, 9 # matrice 3x3, memorizzata riga per riga
.text
la t0, m # indirizzo base
li t1, 3 # numero di righe
li t2, 3 # numero di colonne
li t3, 0 # indice di riga corrente, inizia da 0
li s0, 0 # totale corrente
row_loop:
beq t3, t1, row_done
li t4, 0 # indice di colonna corrente, inizia da 0
col_loop:
beq t4, t2, col_done
mul t5, t3, t2 # indice di riga * numero di colonne
add t5, t5, t4 # + indice di colonna = indice appiattito
slli t5, t5, 2 # offset in byte = indice * 4
add t6, t0, t5 # indirizzo dell'elemento a questa riga e colonna
lw t6, 0(t6) # valore a quell'indirizzo
add s0, s0, t6 # totale += elemento
addi t4, t4, 1 # passa alla colonna successiva
j col_loop
col_done:
addi t3, t3, 1 # passa alla riga successiva
j row_loop
row_done:
mv a0, s0 # stampa il totale: 1+2+...+9 = 45
li a7, 1
ecall
li a7, 10
ecall

Due cicli annidati indicizzano la matrice. Il ciclo esterno seleziona una riga e il ciclo interno seleziona una colonna all'interno di quella riga. moltiplica l'indice di riga per il numero di colonne. Aggiungere l'indice di colonna e scalare per 4 dà l'indirizzo di quell'elemento. Il valore memorizzato a quell'indirizzo viene caricato e aggiunto al totale corrente. Quando il ciclo interno termina una riga, il ciclo esterno passa alla riga successiva, finché tutte e 3 le righe sono state elaborate.

ℹ
L'ordine row-major memorizza tutti i numeri della prima riga, poi tutti i numeri della seconda riga, e continua in quel modello. Questa è una convenzione piuttosto che una regola. Altri linguaggi, tra cui Fortran, usano l'ordine column-major: tutti i numeri della prima colonna, poi tutti i numeri della seconda colonna, e così via. Lo scrittore dei dati e il lettore dei dati devono concordare su quale layout è stato usato. RISC-V legge e scrive all'indirizzo che calcoli.