Ottimizzazione del codice con ARM

Il codice generato dal compilatore dipende molto dalla bontà del compilatore stesso, ma anche dal microprocessore impiegato. In questo articolo si vuole mettere in evidenza il codice generato da ARM ricorrendo ai due diversi operatori incrementali e decrementali. In ogni caso, per le nostre valutazioni ci siamo basati sull’ambiente cross arm-elf-gcc, nella versione 2.95.2. Questa distribuzione utilizza l’ambiente di lavoro GNU C compiler, gcc, ed è liberamente disponibile. In ogni caso, le considerazioni che seguono valgono indipendentemente dall’ambiente di cross-compilazione.

Cicli con operatori incrementali

Il  listato 1 mostra un esempio di una routine scritta in C che incrementa una variabile locale con il contenuto di un array, di 128 locazioni, in memoria.

  calc_array (int *data_input)
{
unsigned int counter;
int value=0;
for (counter=0; counter<128; counter++)
        { value += *(data_input++);
        }
return value;
}
Listato 1
calc_array :
MOV r2,r0         ; r2 = data_input
MOV r0,#0         ; value = 0
MOV r1,#0         ; counter = 0
Calc_array_loop:
LDR r3,[r2],#4    ; r3 = *(data_input++)
ADD r1,r1,#1      ; counter++
CMP r1,#0x80      ; compare counter, 128
ADD r0,r3,r0             ; value += r3
BCC Calc_array_loop      ; if (counter<128) goto loop
MOV pc,r14        ; return value
Listato 2

La lettura del file generato dal compilatore, listato 2, mette in evidenza l’implementazione del ciclo for presente nel listato 1, come vediamo si richiedono tre istruzioni in assembler:

  • l’istruzione  ADD è utilizzata per incrementare il contatore counter;
  • una istruzione cmp (compare) per controllare se il contatore è minore del valore finale (128);
  • un salto su condizionale per permettere al microprocessore di continuare  il ciclo se la condizione risulta essere minore di 128. Da queste considerazioni possiamo dedurre che un’implementazione di questo tipo non è molto efficiente. Infatti, tipicamente il microprocessore  ARM per implementare un ciclo iterativo utilizza solo due istruzioni, cioè:
  • una sottrazione per decrementare il contatore del ciclo;
  • un salto condizionale per saltare all’istruzione da eseguire; Per sfruttare queste prerogative di solito è necessario  scrivere il codice in maniera differente. Per esempio, potrebbe essere utile servirsi di un contatore che converga a zero piuttosto che ad un valore arbitrario, In questo modo, la comparazione con il valore di zero può essere diversamente sfruttato ricorrendo alle cosiddette condition flag.

Cicli con operatori decrementali

Il  listato 3 mostra l’effetto di un ciclo decrementale in luogo di uno incrementale.

Calc_array:
MOV r2,r0         ; r2 = data_input
MOV r0,#0         ; value = 0
MOV r1,#0x80      ; counter = 128
Calc_array_loop:
LDR r3,[r2],#4    ; r3 = *(data_input++)
SUBS r1,r1,#1     ; counter— and set flags
ADD r0,r3,r0             ; value += r3
BNE calc_array_loop      ; if (counter!=0) goto loop
MOV pc,r14        ; return value
Listato 3

Vediamo che le istruzioni SUBS e BNE sono utilizzate per implementare  il ciclo iterativo. Possiamo notare che per un ciclo iterativo, qualora si utilizzi un contatore di tipo unsigned è possibile utilizzare indifferentemente  le condizioni counter!=0 o counter>0, infatti siccome counter non può essere negativo le condizioni essenzialmente acquistano la stessa rilevanza. Viceversa, con un contatore di tipo signed è possibile utilizzare la condizione counter>0 per determinare se continuare o meno il flusso iterativo. Probabilmente  il compilatore  inserirà le seguenti due istruzioni per implementare la struttura del ciclo:

SUBS r1,r1,#1 ; compara counter con 1, counter=counter-1
BGT loop ; if (counter+1>1) goto loop

In questo modo il compilatore inserirà la seguente porzione di codice:

SUB r1,r1,#1 ; counter—
CMP r1,#0 ; compara counter con 0
BGT loop ; if (counter>0) goto loop

A prima vista questo può sembrare inefficiente. Ma in realtà, ciò presuppone una certa accuratezza; infatti, quando il valore di counter è uguale a -0x80000000 le due porzioni di codice generano risposte differenti. Per prima cosa, la porzione di codice con l’istruzione SUB compara counter con 1 e successivamente decrementa counter. In questo modo con - 0x80000000 < 1 il ciclo termina. Secondariamente, si decrementa counter e si compara con 0; secondo l’aritmetica modulo 2 ora counter ha il valore di +0x7fffffff, un valore maggiore di zero: in questo modo il ciclo può continuare per diverse iterazioni. Quello che può succedere ma è una casistica molto rara, quasi impossibile, è che counter prenda un valore di -0x80000000 e il compilatore non è in grado di determinarlo, specie se il ciclo inizia con una variabile che contiene in valore dell’iterazione. Tuttavia, si potrebbe utilizzare la condizione di terminazione counter!=0 per variabili contatori di tipo signed o unsigned. In questo modo si risparmia una istruzione rispetto alla condizione counter>0 per operatori signed.

Suggerimenti

Vediamo alcune considerazioni utili per scrivere strutture iterative efficienti. Per prima cosa è utile tenere presente che utilizzare cicli a variabili di controllo con operatori di decremento verso lo zero è più efficiente. In questo modo il compilatore non ha l’esigenza di utilizzare registri per tracciare  il valore di terminazione con il  controllo di flusso. Inoltre, utilizzare contatori di tipo unsigned per default e condizioni quali counters!=0 invece di counters>0 assicurano un overhead di sole due istruzioni. Un altro aspetto da non sottovalutare è l’uso di cicli do-while in luogo di cicli diversi quando si è a conoscenza di almeno una iterazione. Un ultimo accorgimento è quello di utilizzare il concetto del loop unrolling  (la replicazione del corpo del loop stesso). Utilizzarlo può rappresentare un valido aiuto per ridurre l’overhead del ciclo (test/incremento) e sfruttare al meglio le architetture pipeline. Senz’altro è buona cosa valutare se l’overhead che si risparmia è inferiore, in proporzione, di quello che ci aspettiamo di ricavare: utilizzare lo srotolamento del loop implica un aumento del codice del programma e potrebbero peggiorare le prestazioni della cache.

Conclusione

Come abbiamo visto l’uso degli operatori (incrementali o decrementali) possono influire sulla qualità del codice generato. Per questa ragione occorre valutare attentamente la propria applicazione per scegliere la soluzione migliore.

 

 

Scarica subito una copia gratis

Una risposta

  1. Maurizio Di Paolo Emilio Maurizio 24 Ottobre 2016

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend