cacciatore di taglie: Ehi, lo sai che la tua faccia somiglia a quella di uno che vale duemila dollari?
Biondo (il buono): [comparendo alle loro spalle] Già... ma tu non somigli a quello che li incassa...
(...una premessa: questo post è un remake di un mio vecchio post (parte 1 di 3). Ma, anche se tratta lo stesso argomento, amplia e perfeziona un po' il discorso è mi è sembrato il caso di riproporlo. Leggete e mi direte...)
Il riferimento cinematografico di questo mese calza proprio a pennello: un Variable Length Array (VLA per gli amici) sarebbe perfetto per fare la parte del cattivo nel capolavoro Il buono, il brutto, il cattivo del grande Sergio Leone. E alla fine del (prossimo) articolo sarà chiaro il perché.
![]() |
...ciao sono un VLA: inizia a preoccuparti... |
I VLA sono una cosa relativamente (si, molto relativamente) nuova del C: sono stati introdotti nel C99, e sono, apparentemente, il sogno fatto realtà del mondo C: "Finalmente gli array con dimensione variabile! Ah, se li avessi avuti prima del '99!". Allora: l'idea è semplice, con un VLA potete scrivere cosucce tipo queste:
void myVla(int size1, // un size desiderato del VLAint size2) // un size desiderato del VLA{// il mio VLA di intint ivla[size1];// fai qualcosa con il VLA di int...// il mio VLA bidimensionale di floatfloat fvla[size1][size2]:// fai qualcosa con il VLA bidimensionale di float...}
Fantastico, no? Troppo bello per essere vero... ma ci saranno delle controindicazioni? Sicuramente non nelle prestazioni: ho scritto giustappunto un po' di codice per testare le prestazioni dei VLA rispetto alle alternative più immediate: array dinamici (con malloc(3)) e array fissi (in heap e stack). Vediamolo, no? Vai col codice!
#include <stdio.h>#include <time.h>#include <stdlib.h>#define MYSIZE 1000000// variabile dummy per evitare lo svuotamento totale delle funzioni usando GCC -O2int avoid_optimization;// prototipi localivoid runTest(int iterations, void (*funcptr)(int), int size, const char *name);void testVLA(int size);void testMallocVLA(int size);void testStackFLA(int dum);void testHeapFLA(int dum);// funzione main()int main(int argc, char* argv[]){// test argomentiif (argc != 2) {// errore: conteggio argomenti erratoprintf("%s: wrong arguments counts\n", argv[0]);printf("usage: %s vla iterations [e.g.: %s 10000]\n", argv[0], argv[0]);return EXIT_FAILURE;}// estrae iterazioniint iterations = atoi(argv[1]);// esegue testrunTest(iterations, &testVLA, MYSIZE, "testVLA");runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA");runTest(iterations, &testStackFLA, 0, "testStackFLA");runTest(iterations, &testHeapFLA, 0, "testHeapFLA");// escereturn EXIT_SUCCESS;}// runTest() - funzione per eseguire i testvoid runTest(int iterations, // iterazioni del testvoid (*funcptr)(int), // funzione di testint size, // size dell'arrayconst char *name) // nome funzione di test{// prende start timeclock_t t_start = clock();// esegue iterazioni testfor (int i = 0; i < iterations; i++)(*funcptr)(size);// prende end time e mostra il risultatoclock_t t_end = clock();double t_passed = ((double)(t_end - t_start)) / CLOCKS_PER_SEC;printf("%-13s - Tempo trascorso: %f secondi\n", name, t_passed);}// testVLA() - funzione per eseguire il test del VLAvoid testVLA(int size) // size per VLA{int vla[size];// loop di testfor (int i = 0; i < size; i++)vla[i] = i;// istruzione per evitare lo svuotamento totale della funzione usando GCC -O2avoid_optimization = vla[size / 2];}// testMallocVLA() - funzione per eseguire il test del malloc VLAvoid testMallocVLA(int size) // size per malloc(){int *mallocvla = malloc(size * sizeof(int));// loop di testfor (int i = 0; i < size; i++)mallocvla[i] = i;// istruzione per evitare lo svuotamento totale della funzione usando GCC -O2avoid_optimization = mallocvla[size / 2];free(mallocvla);}// testStackFLA() - funzione per eseguire il test dello stack FLAvoid testStackFLA(int dum) // parametro dummy{int stackfla[MYSIZE];// loop di testfor (int i = 0; i < MYSIZE; i++)stackfla[i] = i;// istruzione per evitare lo svuotamento totale della funzione usando GCC -O2avoid_optimization = stackfla[MYSIZE / 2];}// testHeapFLA() - funzione per eseguire il test dello heap FLAint heapfla[MYSIZE];void testHeapFLA(int dum) // parametro dummy{// loop di testfor (int i = 0; i < MYSIZE; i++)heapfla[i] = i;}
Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo, qualche dettaglio strutturale.
Allora: visto che si tratta di un test comparativo ho scritto una funzione runTest() che chiama n-iterazioni della funzione da testare e conta il tempo impiegato. Il main() si limita a chiamare quattro volte runTest(), una per ogni funzione. Le quattro funzioni di test che ho scritto testano (come richiamato dai nomi, ovviamente): un C99-VLA (la variabile vla), un tradizionale malloc-VLA (la variabile mallocvla), un FLA (Fixed Lengh Array) allocato nello stack (la variabile stackfla) e un FLA allocato nello heap (la variabile heapfla). Per ogni test viene usato un (gran) array-size di 1000000 e il numero di iterazioni si decide al lancio dell'applicazione (questo è molto utile come vedremo tra poco). Ovviamente il malloc-VLA l'ho chiamato così non perché sia un vero e proprio VLA, ma perché rappresenta il modo tradizionale di creare a run-time un array con size "dinamico".
Notare che runTest() usa un function pointer per lanciare il test (avevamo visto qualcosa del genere parlando qui delle callback): ho usato la versione estesa della dichiarazione (void (*funcptr)(int) + passaggio della funzione con l'operatore &) ma vi ricordo che, ad esempio, GCC digerisce facilmente anche la dichiarazione semplificata (void funcptr(int) + passaggio senza l'operatore &). La versione estesa è, ovviamente, più portabile. E visto che siamo in tema di compilatori: anche se i VLA sono ammessi solo da C99 in avanti non c'è bisogno (se usate GCC) di specificare il flag -std=c99 in compilazione: siamo nel 2025 (come passa il tempo!) e le versioni recenti di GCC includono di default (come minimo) anche il C99 (oltre alle estensioni del GNU C).
E, già che ci siamo, facciamo un accenno sul discorso "compatibilità & retrocompatbilità": se proprio volete essere sicuri che quello che avete scritto rispetta uno standard in particolare dovete usare correttamente i flag di compilazione: ad esempio, se volete scrivere usando solo il C89, dovete aggiungere sulla linea di compilazione: -std=c89 -pedantic. Se poi state usando un GCC veramente datato allora la compilazione dell'esempio con i VLA vi darà Warning e/o errori, e dovrete ricompilare forzando (se possibile) la compatibilità col C99.
Notare anche che ho aggiunto, in ogni funzione di test, una semplice istruzione per usare l'array creato (é questa: avoid_optimization = ...), per evitare che, compilando con -O2, l'ottimizzatore del GCC azzeri il contenuto della funzione stessa: infatti, se l'array non lo usa nessuno, il nostro amico GCC si prende la libertà di eliminare (praticamente) la funzione, con il risultato che il test passa in 0 secondi!
E vediamo i risultati!
aldo@Linux $ gcc -O0 vla.c -o vlaaldo@Linux $ ./vla 2000testVLA - Tempo trascorso: 3.918936 seconditestMallocVLA - Tempo trascorso: 2.729077 seconditestStackFLA - Tempo trascorso: 3.648311 seconditestHeapFLA - Tempo trascorso: 3.623842 secondialdo@Linux $ gcc -O2 vla.c -o vlaaldo@Linux $ ./vla 2000testVLA - Tempo trascorso: 0.664499 seconditestMallocVLA - Tempo trascorso: 0.616732 seconditestStackFLA - Tempo trascorso: 0.211779 seconditestHeapFLA - Tempo trascorso: 0.258773 secondi
Come vedete ho eseguito test senza ottimizzazione (con il flag di compilazione -O0 che si può anche omettere visto che è il default) e con ottimizzazione (con il flag di compilazione -O2 ) e, ovviamente, mi è tornato utile il parametro n-iterazioni dell'applicazione, perché mi ha permesso di trovare un valore adatto a ottenere risultati significativi pur evitando tempi di esecuzione biblici per la versione senza ottimizzazioni. Come possiamo commentare? Beh, il VLA se la cava egregiamente, con e senza ottimizzazioni! Ottiene, praticamente, gli stessi risultati del suo diretto concorrente, il malloc-VLA, ed è più semplice da usare!
E allora, tornando sul pezzo: si può dire che il VLA è approvato!
MA PERÒ...
Beh, il però del VLA "cattivo" anticipato sopra ve lo spiegherò meglio nel prossimo articolo, e sappiate che non è tutto oro quello che luccica... e tanto per farvi un piccolo spoiler sulle prossime considerazioni finali: io non uso mai i VLA nel codice che scrivo!
Ciao e al prossimo post!