Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 8 luglio 2017

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

Eccoci, e, come promesso, questo mese parleremo di un parente stretto dei VLAs, ovvero della funzione alloca()... sarà un buono, un brutto o un cattivo?
ciao, sono lo spoiler di questo post!
Allora, ho aggiunto il codice al programma di test per provare la alloca(). E, per non farci mancare niente, ho aggiunto anche del codice per provare la malloc() del C++, ovvero la new (dopo il problematico test di std::vector dello scorso post era doveroso completare il discorso con con qualcosa di più prestante, mica che si dica che ce l'ho con il C++...). Quindi useremo il programma C++ dello scorso post (tanto era praticamente identico alla versione C): vi riporto nuovamente il main() e le due funzioni di test aggiunte (per ricostruire il programma completo basta consultare i due post precedenti e fare un po' di cut-and-paste). 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 GCC -O2
int avoid_optimization;

// prototipi locali
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);
void runTest(int iterations, void (*funcptr)(int), int size, const char *name);

// 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;
}

// funzione testAllocaVLA()
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 GCC -O2
    avoid_optimization = allocavla[size / 2];
}

// funzione testNewVLA()
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 GCC -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 devo neanche dilungarmi in spiegazioni. E i risultati del test? Vediamoli!
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 4.318492 secondi
testMallocVLA -  Tempo trascorso: 3.676805 secondi
testStackFLA  -  Tempo trascorso: 4.339859 secondi
testHeapFLA   -  Tempo trascorso: 4.340040 secondi
testAllocaVLA -  Tempo trascorso: 3.678644 secondi
testVectorVLA -  Tempo trascorso: 10.934088 secondi
testNewVLA    -  Tempo trascorso: 3.679624 secondi
aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp
aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000
testVLA       -  Tempo trascorso: 0.746956 secondi
testMallocVLA -  Tempo trascorso: 0.697261 secondi
testStackFLA  -  Tempo trascorso: 0.696310 secondi
testHeapFLA   -  Tempo trascorso: 0.700047 secondi
testAllocaVLA -  Tempo trascorso: 0.691677 secondi
testVectorVLA -  Tempo trascorso: 1.384563 secondi
testNewVLA    -  Tempo trascorso: 0.695037 secondi
Allora, cosa si può dire? I risultati dei test dei post precedenti li abbiamo già ampliamene commentati, quindi ora possiamo solo aggiungere che: alloca() è molto veloce, visto che è, in pratica, una malloc() 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é, quasi sempre, la new usa internamente la malloc().

Va bene, la alloca() è veloce, ma lo è (solo un po' meno) anche un VLA, e questo non lo ha salvato dal essere eletto come cattivo del film. Quindi dovremo fare di nuovo una lista di pro e contro, e vedere quale parte è più pesante. Vediamo prima i pro:
  1. la alloca() è molto veloce, già che usa lo stack invece del heap.
  2. la alloca() è facile da usare, è una malloc() senza free(). 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 VLAs).
  3. per il motivo visto al punto 2 la alloca() 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() i vantaggi in questo senso sono grandissimi.
  4. a causa della sua implementazione interna (senza entrare in dettagli profondi) la alloca() 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() 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 VLAs.
  2. la alloca() non è molto portatile, visto che non è una funzione standard e il suo funzionamento/presenza dipende molto dal compilatore in uso.
  3. la alloca() è error prone (parte 1): bisogna usarla con attenzione, visto che induce, tipicamente, a errori come usare la variabile allocata quando oramai non è più valida (passarla con un return o inserirla dentro una struttura dati esterna alla funzione, per esempio)... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
  4. la alloca() è error prone (parte 2): ci sono problemi ancora più sottili da considerare nell'uso, ad esempio può risultare MOLTO pericoloso mettere una alloca() dentro un loop o in una funzione ricorsiva  (povero stack!) o in una funzione inline (che usa lo stack in una maniera che si scontra un po' con la maniera di usare lo stack della alloca())... ma noi siamo ottimi programmatori e questo punto non ci spaventa, no?
  5. la alloca() è error prone (parte 3): la alloca() usa lo stack, che è normalmente limitato rispetto allo heap (specialmente negli ambienti embedded che sono molto frequentati 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?
Va beh, conclusioni? Ci sarebbero gli estremi per dichiarare la alloca() come un altro cattivo (stessa sorte del 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 qui sopra?). Comunque usate la alloca() con molta cautela, uomo avvisato mezzo salvato!

Ciao e al prossimo post!