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

I

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

Tuco (il brutto): Il mondo è diviso in due, amico mio: quelli che hanno la corda al collo e quelli che la tagliano. Solo che il collo dentro la corda è il mio, sono io che rischio, perciò la prossima volta voglio più della metà.
Biondo (il buono): Sì, è vero che tu rischi. Ma io taglio e se tu mi abbassi la percentuale... sigaro? ... potrei sbagliare la mira.

Dunque, dove eravamo rimasti? Ah si, nel primo articolo della serie (che avete appena riletto, vero?), avevamo approvato, con riserva, i Variable Length Array (VLA per gli amici), che sono facili da usare, utili e con ottime prestazioni. Poi, nel secondo articolo (anche quello è da rileggere, eh!), avevamo, ahimè, confermato la riserva assegnandogli il ruolo del cattivo del mitico Il buono, il brutto, il cattivo del grande Sergio Leone, e questo perché i contro dei VLA superavano ampliamene i pro. Ed ora eccoci di nuovo sul pezzo e, come promesso, oggi parleremo di un parente stretto dei VLA, ovvero della funzione alloca(3)sarà un buono, un brutto o un cattivo?

Il buono, il brutto, il VLA
…ciao, sono lo spoiler di questo post!…

E allora: ho aggiunto del codice al programma di test (visto nei precedenti articoli) per provare la alloca(3). E, per non farci mancare niente, ho aggiunto anche del codice per provare la versione C++ della malloc(3), ovvero la new (ebbene si: dopo il problematico test di std::vector dello scorso post era doveroso parlare anche di qualcosa più prestante, mica si dica che ce l’ho con il C++…). Quindi useremo il programma C++ già proposto (tanto era praticamente identico alla versione C) di cui vi riporterò solamente il main() e le due funzioni di test aggiunte (e, per ricostruire il programma completo, basta consultare i due post precedenti e fare un po’ di copia-e-incolla). Vai col codice!

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <vector>

#define MYSIZE  1000000

// variabile dummy per evitare lo svuotamento totale delle funzioni usando g++ -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);
void testAllocaVLA(int size);
void testVectorVLA(int size);
void testNewVLA(int size);

// 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");
    runTest(iterations, &testAllocaVLA, MYSIZE, "testAllocaVLA");
    runTest(iterations, &testVectorVLA, MYSIZE, "testVectorVLA");
    runTest(iterations, &testNewVLA, MYSIZE, "testNewVLA");

    // esce
    return EXIT_SUCCESS;
}

// testAllocaVLA() - funzione per eseguire il test della alloca VLA
void testAllocaVLA(
    int size)       // size per alloca()
{
    int *allocavla = (int*)alloca(size * sizeof(int));

    // loop di test
    for (int i = 0; i < size; i++)
        allocavla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando g++ -O2
    avoid_optimization = allocavla[size / 2];
}

// testNewVLA() - funzione per eseguire il test della new VLA
void testNewVLA(
    int size)       // size per new
{
    int *newvla = new int[size];

    // loop di test
    for (int i = 0; i < size; i++)
        newvla[i] = i;

    // istruzione per evitare lo svuotamento totale della funzione usando g++ -O2
    avoid_optimization = newvla[size / 2];

    delete[] newvla;
}

Come potete vedere, le due funzioni aggiunte sono perfettamente allineate stilisticamente con le altre che avevo già proposto e sono, come sempre, iper-commentate, così non c’è neanche bisogno di dilungarmi in spiegazioni. E i risultati del test? Vediamoli!

aldo@Linux $ g++ -O0 vlacpp.cpp -o vlacpp
aldo@Linux $ ./vlacpp 2000
testVLA       -  Tempo trascorso: 3.908325 secondi
testMallocVLA -  Tempo trascorso: 2.724537 secondi
testStackFLA  -  Tempo trascorso: 3.631790 secondi
testHeapFLA   -  Tempo trascorso: 3.623486 secondi
testAllocaVLA -  Tempo trascorso: 2.751826 secondi
testVectorVLA -  Tempo trascorso: 8.930886 secondi
testNewVLA    -  Tempo trascorso: 2.752857 secondi

aldo@Linux $ g++ -O2 vlacpp.cpp -o vlacpp
aldo@Linux $ ./vlacpp 2000
testVLA       -  Tempo trascorso: 0.633613 secondi
testMallocVLA -  Tempo trascorso: 0.627741 secondi
testStackFLA  -  Tempo trascorso: 0.267571 secondi
testHeapFLA   -  Tempo trascorso: 0.263820 secondi
testAllocaVLA -  Tempo trascorso: 0.623920 secondi
testVectorVLA -  Tempo trascorso: 0.773795 secondi
testNewVLA    -  Tempo trascorso: 0.613130 secondi

Allora, cosa si può dire? I risultati dei test dei post precedenti li abbiamo già ampiamente commentati, quindi ora possiamo solo aggiungere questo: alloca(3) è molto veloce, visto che è, in pratica, una malloc(3) nello stack (e, usandola in maniera appropriata, potrebbe/dovrebbe essere la più veloce del gruppo). E la new? Beh, si comporta (come previsto) benissimo, anche perché, spesso, la new usa internamente la malloc(3).

E va bene: la alloca(3) è veloce, ma lo è (solo un po’ meno) anche un VLA, e questo non lo ha salvato dal essere eletto come cattivo nello scorso articolo Quindi ci toccherà fare di nuovo una lista di pro e contro, e vedere quale parte pesa di più. Vediamo prima i pro:

  1. La alloca(3) è molto veloce, già che usa lo stack invece del heap.
  2. È anche facile da usare, visto che è, praticamente, una malloc(3) senza free(3). La variabile allocata ha uno scope a livello di funzione, quindi rimane valida fino a quando la funzione ritorna al chiamante, esattamente come una qualsiasi variabile automatica locale (anche un VLA funziona più o meno così, ma il suo scope è a livello di blocco, non di funzione, e questo è, probabilmente, un punto a favore dei VLA).
  3. Per il motivo visto al punto 2 la alloca(3) non lascia in giro residui di memoria in caso di errori gravi nella attività di una funzione (e con malloc + free non è altrettanto facile realizzare questo). Se poi siete soliti a usare cosucce come longjmp(3) i vantaggi in questo senso sono grandissimi.
  4. A causa della sua implementazione interna (senza entrare in dettagli profondi) la alloca(3) non causa frammentazione della memoria.

Uh, che bello! E i contro?

  1. La gestione degli errori è problematica, perché non c’è maniera di sapere se alloca(3) ha allocato bene o ha provocato un stack overflow (in questo caso provoca effetti simili a quelli di un errore per ricorsione infinita)… uh, questo è esattamente lo stesso problema dei VLA.
  2. Non è molto portabile, visto che non è una funzione standard e il suo funzionamento e la sua presenza dipendono molto dal compilatore in uso.
  3. È error prone per almeno tre motivi:
    • Bisogna usarla con attenzione, visto che induce, tipicamente, a errori come usare la variabile allocata quando oramai non è più valida (ritornarla o inserirla dentro una struttura dati esterna alla funzione, per esempio)… ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
    • Ci sono problemi ancora più sottili da considerare nell’uso, ad esempio può risultare MOLTO pericoloso mettere una alloca(3) dentro un loop o in una funzione ricorsiva  (povero stack!) oppure in una funzione inline (visto che una inline usa lo stack in una maniera che si scontra un po’ con la maniera di usare lo stack della alloca(3))… ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
    • Usa lo stack, che è normalmente limitato rispetto allo heap (specialmente nella programmazione embedded che è molto frequentata dai programmatori C…). Quindi esaurire lo stack e provocare uno stack overflow è facile (e difficile da controllare, vedi il punto 1)… ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?

E vabbè, conclusioni? Ci sarebbero gli estremi per dichiarare la alloca(3) come un altro cattivo (la stessa sorte dei VLA) ma, dati i notevoli pro e, soprattutto, dato che oggi sono di buon umore, la dichiareremo solo come brutto (visto lo spoiler nella figura iniziale?). Comunque, mi raccomando: usate la alloca(3) con molta cautelauomo avvisato mezzo salvato!

Ciao e al prossimo post!

A proposito di me

Aldo Abate

È un Cinefilo prestato alla Programmazione in C. Per mancanza di tempo ha rinunciato ad aprire un CineBlog (che richiede una attenzione quasi quotidiana), quindi ha deciso di dedicarsi a quello che gli riesce meglio con il poco tempo a disposizione: scrivere articoli sul suo amato C infarcendoli di citazioni Cinematografiche. Il risultato finale è ambiguo e spiazzante, ma lui conta sul fatto che il (si spera) buon contenuto tecnico induca gli appassionati di C a leggere ugualmente gli articoli ignorando la parte Cinefila.
Ma in realtà il suo obiettivo principale è che almeno un lettore su cento scopra il Cinema grazie al C...

Di Aldo Abate

Gli articoli più letti

Articoli recenti

Commenti recenti