Esempio RISC-V (3) - Inversione di un array

L'esempio che tratteremo riguarda l'inversione dell'ordine di un array di interi con un certo numero n di posizioni. Per portare a termine questo compito sfrutteremo due procedure, una procedura invert e una procedura swap che è usata da invert per scambiare due elementi.

Idea
...

Supponiamo di avere un array di 5 elementi:

Indici01234
Elementi1112131415

Quello che vogliamo fare è utilizzare una funzione: invert(array, size) che prede come parametri il riferimento in memoria dell'array e la sua dimensione (numero di elementi), nel nostro caso invert prenderà come parametri un certo indirizzo in memoria e il numero 5.

invert avrà due variabili (registri) che hanno come valore gli estremi dell'array, quindi 0 e 4 (non 5 perché sarebbe il sesto elemento), effettuerà poi un ciclo di questo tipo:

invert:
	i = indiceSinistro
	j = indiceDestro
	while(i < j):
		swap(array, i, j)
		i = i + 1
		j = j - 1
	end while

la funzione invert scambia gli elementi agli estremi dell'array, poi l'indice i incrementa di 1, mentre l'indice j decrementa di 1. In questo modo gli indici man mano si avvicineranno.

invert resterà nel ciclo fino a che i non sarà almeno uguale a j, nel momento in cui i due indici si incontrano il ciclo termina e l'array sarà stato invertito.

invert si serve della funzione swap che prende l'array e gli indici degli elementi da scambiare.

In questo contesto ci rendiamo subito conto di due cose:

  • swap() è una procedura foglia
  • invert() non è una procedura foglia

Convenzioni di chiamata del RISC-V
...

  • i registri a0-a7 sono utilizzati per passare parametri alle funzioni, in particolare il registro a0 è usato come valore di ritorno;
  • i registri s0-s11 sono registri preservati: chi chiama la procedura può assumere che tali registri rimarranno in tatti: la procedura chiamata infatti può usarli, ma solo dopo aver salvato il loro contenuto sullo stack;
  • i registri t0-t6 sono registri non preservati: chi chiama la procedura deve considerare che la procedura chiamata potrebbe utilizzare quei registri senza preoccuparsi di salvarne il contenuto dunque il chiamante deve preoccuparsi di salvare il loro contenuto nello stack.

In genere una procedura foglia non deve preoccuparsi di salvare nulla, può usare i registri temporanei (t) senza preoccuparsi di salvarli, tuttavia potrebbe verificarsi che i registri non bastino per svolgere il compito e che servano altri registri, in tal caso potrebbe essere necessario salvare qualche registro s nello stack ed usarlo.

Invece una procedura non foglia, in molti casi è costretta a salvare nello stack e ripristinare dallo stack il contenuto dei registri usati.

In questo esempio affronteremo entrambe le tipologie di procedure.

Procedura foglia: swap(array, x, y)
...

  • a0: avrà l'indirizzo in memoria dell'array
  • a1: avrà il valore dell'indice sinistro
  • a2: avrà il valore dell'indice destro

NOTA: gli elementi dell'array sono di tipo word

swap:
	# a1, a2 sono gli indici 
	# a0 è l'indirizzo in memoria dell'array
	# non ha senso copiare questi valori in dei registri
	addi t0, zero, 4 # abbiamo a che fare con delle word (4 byte)

	# moltiplichiamo gli indici per 4 (1 word)
	mul a1, a1, t0
	mul a2, a2, t0

	# sommiamo all'indirizzo base dell'array l'indice moltiplicato per 4
	# otterremo così l'indirizzo degli elementi che ci interessano
	add t3, a0, a1 # t3 contiene l'indirizzo di a0[a1]
	add t4, a0, a2 # t4 contiene l'indirizzo di a0[a2]
	
	# adesso da quegli indirizzi dobbiamo caricare (load) i valori
	lw t1, 0(t3)
	lw t2, 0(t4)

	# t5 fungerà da registro di appoggio per effettuare lo scambio
	add t5, zero, t2
	add t2, zero, t1
	add t1, zero, t5

	# salviamo i cambiamenti fatti nell'array
	sw t2, 0(t4)
	sw t1, 0(t3)
	
	ret

Avanzamento degli elementi dell'array

Indici01234
Elementi1112131415
Dimensione per cella1 word1 word1 word1 word1 word

a0 è un indirizzo che punta alla base dell'array, per base intendiamo la 0-esima posizione.
Se voglio accedere alle terza posizione dell'array, quello che devo fare è scostarmi di tre posizioni, tuttavia bisogna ricordarsi che ciascuna posizione è una word (4 byte), quindi devo moltiplicare le posizioni di cui mi voglio scostare per 4 e sommarli all'indirizzo base:

addi t0, zero, 3 # metto 3 in t0
addi t1, zero, 4 # metto 4 in t1
mul t2, t0, t1 # moltiplico 4 per 3
add a0, a0, t2 # sommo il risultato della moltiplicazione ad a0

Tale valore mi restituisce l'indirizzo della posizione 3, a quel punto non mi resta che caricare quel valore dalla memoria: lw t3, 0(a0).

Procedura non foglia: invert(array, size)
...

  • a0: conterrà l'indirizzo dell'array
  • a1: conterrà la dimensione dell'array

invert è una funzione non foglia, il che vuol dire che chiamerà qualche funzione, sarà necessario salvare da qualche parte il return address (ra), ci si accorge nel corso dello sviluppo quali registri vanno salvati nello stack, supponiamo di voler salvare a prescindere l'indirizzo dell'array e la size.

In questo caso se usassimo i registri t per svolgere le operazioni, sarebbe necessario mantenere aggiornato nello stack il valore di quel registro, per non perderlo (dato che gli indici aumentano o diminuiscono), mentre possiamo decidere anche di usare i registri s in modo tale da risparmiarci di salvare in corso d'opera il loro valore, poiché le funzioni chiamate devono mantenere in tatti tali registri. Tuttavia dobbiamo prima, appunto, salvare il loro valore nello stack (anche se sappiamo che nessuno li ha usati/userà).
Essendo chiamata un'altra funzione (che in questo caso non ritorna niente) il valore di a0 potrebbe essere sovrascritto, salviamo anche quello.

Consiglio: quando apriamo lo stack per un certo numero di valori, conviene scrivere sin da subito il recupero di tali valori dallo stack.

invert: 
	# estendiamo lo stack per: ra, s0, s1, a0
	addi sp, sp, -32
	sd ra, 0(sp)
	sd a0, 8(sp)
	sd s0, 16(sp)
	sd s1, 24(sp)

	add s0, zero, zero # indice sinsitro
	add s1, zero, a1 # indice destro
	addi s1, s1, -1 # indice destro - 1

	while:
		bge s0, s1, end
		# carico dalla memoria l'indirizzo dell'array (a0) e lo metto in a0 per passarlo
		# alla procedura swap come parametro
		ld a0, 8(sp)
		add a1, zero, s0 # passo l'indice sinistro come parametro
		add a2, zero, s1 # passo l'indice destro come parametro

		jal ra, swap # chiamata funzione swap

		addi s0, s0, 1 # indice sinistro avanza
		addi s1, s1, -1 # indice destro indietreggia
		
		j while # jump all'etichetta while

	end:
		ld ra, 0(sp)
		ld a0, 8(sp)
		ld s0, 16(sp)
		ld s1, 24(sp)
		addi sp, sp, 32
		ret