Il buono, il brutto, il VLA – come usare i Variable Length Arrays in C – pt.1

  ICT, Rassegna Stampa
image_pdfimage_print
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...

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é.

Il buono, il brutto, il VLA
…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 VLA int size2) // un size desiderato del VLA
{ // il mio VLA di int int ivla[size1]; // fai qualcosa con il VLA di int ... // il mio VLA bidimensionale di float float 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 -O2
int avoid_optimization; // prototipi locali
void 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 argomenti if (argc != 2) { // errore: conteggio argomenti errato printf("%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 iterazioni int iterations = atoi(argv[1]); // esegue test runTest(iterations, &testVLA, MYSIZE, "testVLA"); runTest(iterations, &testMallocVLA, MYSIZE, "testMallocVLA"); runTest(iterations, &testStackFLA, 0, "testStackFLA"); runTest(iterations, &testHeapFLA, 0, "testHeapFLA"); // esce return EXIT_SUCCESS;
} // runTest() - funzione per eseguire i test
void runTest( int iterations, // iterazioni del test void (*funcptr)(int), // funzione di test int size, // size dell'array const char *name) // nome funzione di test
{ // prende start time clock_t t_start = clock(); // esegue iterazioni test for (int i = 0; i < iterations; i++) (*funcptr)(size); // prende end time e mostra il risultato clock_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 VLA
void testVLA( int size) // size per VLA
{ int vla[size]; // loop di test for (int i = 0; i < size; i++) vla[i] = i; // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2 avoid_optimization = vla[size / 2];
} // testMallocVLA() - funzione per eseguire il test del malloc VLA
void testMallocVLA( int size) // size per malloc()
{ int *mallocvla = malloc(size * sizeof(int)); // loop di test for (int i = 0; i < size; i++) mallocvla[i] = i; // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2 avoid_optimization = mallocvla[size / 2]; free(mallocvla);
} // testStackFLA() - funzione per eseguire il test dello stack FLA
void testStackFLA( int dum) // parametro dummy
{ int stackfla[MYSIZE]; // loop di test for (int i = 0; i < MYSIZE; i++) stackfla[i] = i; // istruzione per evitare lo svuotamento totale della funzione usando GCC -O2 avoid_optimization = stackfla[MYSIZE / 2];
} // testHeapFLA() - funzione per eseguire il test dello heap FLA
int heapfla[MYSIZE];
void testHeapFLA( int dum) // parametro dummy
{ // loop di test for (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ù portatile. 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 VLAs 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 vla
aldo@Linux $ ./vla 2000
testVLA - Tempo trascorso: 3.918936 secondi
testMallocVLA - Tempo trascorso: 2.729077 secondi
testStackFLA - Tempo trascorso: 3.648311 secondi
testHeapFLA - Tempo trascorso: 3.623842 secondi aldo@Linux $ gcc -O2 vla.c -o vla
aldo@Linux $ ./vla 2000
testVLA - Tempo trascorso: 0.664499 secondi
testMallocVLA - Tempo trascorso: 0.616732 secondi
testStackFLA - Tempo trascorso: 0.211779 secondi
testHeapFLA - 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 e 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!

https://italiancoders.it/il-buono-il-brutto-il-vla-come-usare-i-variable-length-arrays-in-c-pt-1/