In questo articolo prenderemo in esame una libreria che consentirà di ottimizzare i tempi di esecuzione di calcoli in floating point su Arduino.
Come abbiamo visto nel prececente articolo dedicato all’ottimizzazione di Arduino, l’assenza di un coprocessore matematico o di una FPU integrata nel microcontrollore rende i calcoli sui numeri reali piuttosto pesanti per il nostro piccolo sistema. Mentre infatti i numeri interi (int) utilizzano una rappresentazione binaria che consente di mappare quattro miliardi di valori sui 32 bit dell’hardware (esattamente 232 – 1), i numeri in virgola mobile (float e double) utilizzano una mappatura hardware decisamente più complessa: un bit per il segno, n bit per l’esponente ed m bit per la mantissa, che determina la precisione delle cifre significative. Questa configurazione consente di spingere il tipo float a rappresentare valori ben superiori a 232, a costo però di una perdita nella precisione del numero rappresentato a 6 cifre significative.
Una rappresentazione simile a livello hardware comporta inevitabilmente una serie di “aggiustamenti” software da parte di apposite librerie matematiche per consentire di convertire i tipi float e utilizzarli assieme agli interi nelle somme e nei confronti; è a causa di tali aggiustamenti on-the-fly che le operazioni sui float sono tanto più lente che sugli int. Le librerie di runtime sono quelle che consentono di lavorare con tipi di dato in virgola mobile quando non sia presente alcuna FPU.
E non basta. A causa della limitata precisione, quando valori estremamente piccoli vengono sommati a valori estremamente grandi, i valori estremamente piccoli scompaiono. “Il numero grande mangia il numero piccolo” a causa di una granularità crescente man mano che si utilizzino numeri più grandi. Fortunatamente lo standard IEEE754 non è l’unico che consenta di rappresentare valori in virgola mobile.
Un sistema di conversione per valori in virgola mobile dotato di linear encoding (che cioè non perde la propria granularità con il crescere dei valori) è dato dalla libreria FloatingPoints, che consente di utilizzare ad esempio lo standard Q7.8 (esistono anche gli standard Q5.10 e Q1.14).
L’immagine mostra la definizione per la codifica Q8.8 (senza segno). La Q7.8 è identica, ma prevede il bit per la definizione del segno come MSB.
Ora, in alcuni casi i numeri in fixed point possono risultare la scelta ottimale, specie per sistemi non dotati di unità di calcolo in virgola mobile. Ovviamente a costo di una ridotta estensione nella rappresentazione dei valori: con 8 bit possiamo rappresentare al massimo i valori in virgola mobile tra 0 e 255, o tra -128 e + 127, ma in alcuni casi il range è più che sufficiente.
Per testare il codice presentato in questo articolo faremo uso della libreria per Arduino FixedPoint, cui fanno riferimento le direttive #include di riga 1 e 2 del codice.
Per configurare il nostro ambiente dovremo selezionare la voce di menù “Sketch/ Include libreria/Gestione libreria“.
Premendo il pulsante installa, la libreria sarà automaticamente installata all’interno dell’IDE di Arduino.
Di seguito il codice sorgente che verrà utilizzato.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include <FixedPoints.h> #include <FixedPointsCommon.h> /* Benchmark Float vs FixedPoints Esegue migliaia di calcoli frazionari nei due metodi. */ #define USE_FIXED_POINT // Commentare per lavorare con i float #define MEASURE #ifdef USE_FIXED_POINT SQ7x8 a,b,c; #else volatile float a,b,c; #endif #ifdef MEASURE uint32_t StartTime, CurrentTime; #endif void setup() { Serial.begin(9600); a=2.5; b=2.9; c=1.1; } void loop() { uint32_t i = 30000; #ifdef MEASURE StartTime = millis(); #endif while (i--){ a=(a*c+b*c-b/c)*c; if(a>100) a=a/10; } #ifdef MEASURE CurrentTime = millis(); Serial.print("Impiegati "); Serial.print((float)(CurrentTime - StartTime)*0.001,3); Serial.print(" secondi\n"); #endif } |
Dopo la sezione di inclusione degli header della nostra libreria, troviamo le definizioni che ci consentiranno di effettuare una compilazione condizionale del codice (ne abbiamo parlato nell’articolo sull’ottimizzazione). Nel caso particolare, eseguiremo il test configurando le tre variabili a, b, c prima come float, poi come Q7.8 per vedere se sia possibile ottimizzare i tempi di esecuzione del codice.
La costante MEASURE determinerà l’utilizzo del serial monitor per la stampa dei risultati. Il corpo del nostro calcolo verrà ripetuto 30.000 volte per ciascuna configurazione. L’istruzione
1 2 |
if(a>100) a=a/10; |
ci garantirà che il risultato sarà sempre e comunque corretto all’interno del range definito dal tipo Q7.8.
Risultati
Abbiamo elaborato per 100 volte i 30.000 cicli del kernel di calcolo, sommato i risultati e calcolato una media dei tempi di esecuzione nei due casi presi in considerazione:
- Caso 1 (floating point) – 2.584″
- Caso 2 (fixed point) – 0.344″
L’utilizzo della libreria FixedPoints risulta di 7 volte e mezzo più veloce della floating point standard. In alcuni casi particolari tale libreria rappresenta la differenza tra il poter eseguire un lavoro e l’impossibilità di avere risultati coerenti in un tempo sufficiente.
Considerazioni finali
L’articolo a questo punto può dirsi concluso. Abbiamo studiato assieme le differenti configurazioni di bit relative ai diversi tipi di dato, studiato una nuova configurazione di numero a virgola fissa, installato una libreria per Arduino, eseguito una serie di benchmark e trovato una soluzione al classico problema del rallentamento del codice nell’operare sui numeri reali.
Spero che i nostri lettori lo abbiano trovato interessante, e che ci scrivano per proporci nuove idee di ottimizzazione del codice.
Nel frattempo un saluto a tutti e a risentirci al prossimo articolo!
ciao 7 volte piu’ veloce e’ troppo importante dalla prossima applicazione provero’ a usarli ,per favore sai quali sono i limiti ? (per i float sono 3,4028235e+38 cioe’ 2 ^ 128 = 3,4 x 10 ^ 38 )
Purtroppo non è così semplice ed immediato.
Come avrai notato, ho lavorato con numeri piuttosto piccoli e raccolti: il formato Q8.8 consente di utilizzare al massimo 8 bit per la codifica della parte intera e 8 bit per la codifica della parte decimale, quindi hai valori da 0 a 255 con una precisione del 4 per mille della parte decimale. Ottimo se devii lavorare su valori tra 0 e 100, ma evidentemente limitante in caso contrario. D’altra parte la legge sull’Entropia ci insegna che non esistono pasti gratis.
Però…
Però c’è da dire che i formati in virgola fissa sono più d’uno (basta dare un’occhiata al codice della libreria), e in diversi casi è possibile gestire il problema… spostando la precisione del numero con la posizione della virgola.