Esaminiamo alcuni trucchi di programmazione che garantiranno benchmark più efficienti nella esecuzione del codice relativo a progetti basati su Arduino.
Oramai lo abbiamo imparato a memoria: Arduino Uno è basato sul microcontrollore ATmega328 con clock a 16 MHz.
Un clock a 16 MHz su un microcontrollore rappresenta una scelta oculata, in equilibrio tra efficienza e consumi. È noto infatti che in genere la programmazione di questi sistemi è relativamente semplice. Ma cosa accade quando il nostro codice deve ricorrere a pesanti routine matematiche? Non disponendo di una FPU (floating point unit) integrata, Arduino deve ricorrere ad una libreria di emulazione per eseguire calcoli complessi su numeri in virgola mobile (ad esempio elevazione a potenza, funzioni trigonomertriche o logaritmiche). Purtroppo tali funzioni sono proprio quelle in genere necessarie per il calcolo di parametri operativi sui circuiti elettronici, sulla misura degli angoli di rotazione dei servomotori e così via.
Compilazione condizionale
Prima di analizzare il codice, due parole sulla compilazione condizionale.
Arduino ci consente di includere o escludere parte del codice dalla compilazione/esecuzione attraverso la definizione di costanti all’interno del codice stesso. Ad esempio:
1 2 3 4 5 6 7 8 9 |
#define CONDIZIONE1 ... ... #ifdef CONDIZIONE1 ... #else ... #endif |
In questo caso, se la costante CONDIZIONE1 è definita, viene eseguito il codice che segue la label #ifdef, altrimenti viene eseguito quello che segue la label #else.
La compilazione condizionale si dimostra molto utile qualora si desideri compilare codice per più microcontrollori, o avere path di esecuzione differenti (come nel nostro caso).
Tabelle di lookup
Immaginiamo di aver bisogno di lavorare su funzioni trigonometriche. Dalle superiori ricordiamo che, per calcolare il seno di un angolo, ci è sufficiente conoscere i valori tra zero e 90°: tutti gli altri valori si trovano per rotazione o riflessione.
Lavorando con valori interi per gli angoli, è semplice precalcolarci i valori del seno tra zero e 90° ed inserirli in un array di float. L’array sarà definito const, in modo da utilizzare la mappatura dei valori direttamente in ROM, senza sprecare RAM preziosa. Allo stesso modo potremo calcolare la tabella con i valori dei logaritmi (in qualsiasi base) degli interi tra 1 e 255.
E qualora ci occorresse un valore non intero, potremmo utilizzare una interpolazione (proporzione) lineare dei risultati tra due interi consecutivi, che per piccoli valori riesce a sostituire egregiamente la funzione originale.
Ma è venuto il momento di dare un’occhiata al codice.
Il programma, disponibile a questo link, è suddiviso in sezioni.
La prima sezione contiene i simboli di definizione che ci consentiranno una compilazione condizionale del codice.
La seconda sezione contiene le tabelle di riferimento per i valori precalcolati di logaritmo e radice quadrata dei numeri interi tra 0 e 255 (ovviamente per il logaritmo di 0 poniamo un valore che corrisponde a meno infinito).
La sezione di setup si occupa semplicemente di configurare il monitor seriale che useremo per visualizzare i risultati.
il loop, infine, conterrà il corpo del nostro benchmark. A seconda delle scelte condizionali effettuate, avremo le seguenti possibilità:
Caso 1 – Misura dei tempi di calcolo su 256 * 500 = 128.000 operazioni di calcolo di logaritmo tramite la libreria math.h.
Caso 2 – Misura dei tempi di calcolo su 256 * 500 = 128.000 operazioni di calcolo di radice quadrata tramite la libreria math.h.
Caso 3 – Misura dei tempi di calcolo su 256 * 500 = 128.000 operazioni di calcolo di logaritmo tramite la tabella di riferimento.
Caso 4 – Misura dei tempi di calcolo su 256 * 500 = 128.000 operazioni di calcolo di radice quadrata tramite la tabella di riferimento.
In ciascuno dei casi possibili avremo la possibilità di stampare i risultati sul monitor seriale.
Tirando le somme, abbiamo quindi i seguenti risultati:
Tempo (in secondi) | |
---|---|
Caso 1 | 19,227 |
Caso 2 | 4,188 |
Caso 3 | 0,001 |
Caso 4 | 0,001 |
Il guadagno netto corrisponde a circa 20.000 volte per il calcolo del logaritmo, ed a 4.000 volte per il calcolo della radice quadrata. Niente male!
Da notare infine l’artificio di sommare 1 alla x nell’ultima riga del loop.
L’IDE di Arduino è infatti talmente orientato all’ottimizzazione da saltare direttamente (senza compilare) l’intero loop qualora il risultato della variabile calcolata (la x, appunto) non fosse utilizzato in nessuna altra parte del programma. E se non ci credete, provate a controllare i risultati del benchmark dopo aver commentato l’ultima riga e ricompilato…
Il codice sorgente utilizzato per questo articolo può essere scaricato dal nostro repository.
Se l’ottimizzazione del codice per Arduino vi interessa, restate in ascolto: stiamo preparando un nuovo articolo al riguardo.