non fidatevi del VLA, parola del buono! |
- la gestione degli errori è problematica, perché non c'è maniera di sapere se il VLA è stato allocato bene o ha provocato un stack overflow (in questo caso provoca effetti simili a quelli di un errore per ricorsione infinita).
- il size del VLA si decide a run-time, quindi il compilatore deve fare dei giochi un po' strani: in base all'implementazione è possibile che una parte (anche importante) dello stack di una funzione venga riservato per un VLA, limitando molto la memoria locale disponibile. Quindi lo stack overflow è sempre dietro l'angolo.
- la portabilità del codice va un po' a farsi benedire: il codice diventa molto compiler-dependent e, soprattutto, visto che una buona fetta di programmatori C scrivono anche codice per sistemi embedded (dove lo stack è, spesso, limitato) risulta complicato il porting di funzioni da applicazioni normali a applicazioni embedded. Funzioni che, magari, smetterebbero di funzionare per motivi misteriosi (beh, neanche tanto misteriosi).
- dulcis in fundo: forse per i motivi appena elencati (o per altri ancora) da C11 in avanti i VLAs sono opzionali e subordinati a una variabile del compilatore __STDC_NO_VLA__: brutto segno.
E adesso ci tocca cercare qualcuno che sia adatto ai ruoli del buono e del brutto. Ecco, per il buono non c'è problema, il candidato ideale è la cara, buona, vecchia malloc() che è sempre una garanzia ed è uscita molto bene dal test. Sulla malloc() è inutile dilungarci, è un punto fermo del C e ne abbiamo già parlato abbondantemente qui.
E il brutto? Beh, per cercare uno adatto dovremo, ahimè, addentrarci nel lato oscuro della forza, e cioè in territorio C++...
(...apro una parentesi: non parlo mai di argomenti che non conosco, perché penso che sia stupido farlo. Per fare un esempio: io non capisco niente di moto e, vi assicuro, nessuno ha mai avuto l'onore di sentirmi disquisire sul mondiale di MotoGP. Seguo una filosofia, che, sfortunatamente, non è seguita da molta gente, è cioè: "meglio stare zitti che parlare solo per dare aria alla bocca". Proprio in virtù di questa coerenza penso di avere i titoli per parlare del C++: lo uso in parallelo al mio amato C da quasi trenta (!) anni, e, modestia a parte, penso si saperlo usare bene. Quindi ne posso disquisire, nel bene e nel male. Chiudo la parentesi...).
Allora, ho ripreso pari pari l'esempio C del post precedente e (facendo il minimo sindacale di modifiche) l'ho trasformato in codice C++, per poter, così, aggiungere un test nuovo che usa std::vector (questo è un oggetto particolarmente caro ai C++ lovers, che lo usano anche per condire l'insalata). Per non ripetere tutto il codice dello scorso post vi riporto solo il main() e la nuova funzione di test aggiunta (il resto è, praticamente, invariato). 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 testVLA(int size); void testMallocVLA(int size); void testStackFLA(int dum); void testHeapFLA(int dum); void testVectorVLA(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, &testVectorVLA, MYSIZE, "testVectorVLA"); // esce return EXIT_SUCCESS; } // funzione testVectorVLA() void testVectorVLA( int size) // size per std::vector { std::vector<int> vectorvla(size); // loop di test for (int i = 0; i < size; i++) vectorvla[i] = i; // istruzione per evitare lo svuotamento totale della funzione usando G++ -O2 avoid_optimization = vectorvla[size / 2]; }(...Nota: grazie alla segnalazione di Ponchietto, un lettore attento e preparato, mi sono reso conto che avevo dimenticato di aggiungere, in ogni funzione di test, una semplice istruzione che usasse l'array creato, così da evitare che l'ottimizzatore del G++ azzerasse il contenuto della funzione stessa (visto che l'array non lo usava nessuno). Ovviamente così anche i risultati dei test con ottimizzazione cambiano. Adesso il post ha codice e risultati rinnovati...)
Compilando (con/senza ottimizzazioni) ed eseguendo questo codice i risultati sono i seguenti:
aldo@ao-linux-nb:~/blogtest$ g++ vlacpp.cpp -o vlacpp aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000 testVLA - Tempo trascorso: 4.274441 secondi testMallocVLA - Tempo trascorso: 3.641508 secondi testStackFLA - Tempo trascorso: 4.340430 secondi testHeapFLA - Tempo trascorso: 4.312986 secondi testVectorVLA - Tempo trascorso: 10.660610 secondi aldo@ao-linux-nb:~/blogtest$ g++ -O2 vlacpp.cpp -o vlacpp aldo@ao-linux-nb:~/blogtest$ ./vlacpp 2000 testVLA - Tempo trascorso: 0.768702 secondi testMallocVLA - Tempo trascorso: 0.694418 secondi testStackFLA - Tempo trascorso: 0.682241 secondi testHeapFLA - Tempo trascorso: 0.694299 secondi testVectorVLA - Tempo trascorso: 1.364321 secondiCome si è comportato std::vector? direi che i numeri parlano da soli... va beh, mettiamola giù in maniera diplomatica: diciamo che abbiamo due notizie, una buona e una cattiva:
- la buona notizia è che il C++ è efficiente come il C (e su questo non avevo dubbi), infatti il nostro programma C trasformato in C++ ottiene (nei primi quattro test) le stesse prestazioni (andate a controllare la, se non ci credete, eh!).
- la brutta notizia è che il C++ è efficiente come il C, ma solo se lo usate come il C, quindi niente STL e ammennicoli vari.
Senza dilungarmi troppo (magari un giorno scriverò un post specifico sull'argomento) vi espongo la mia opinione: il C++ è un grande linguaggio potente, efficiente ed espressivo (è parente stretto del C!), con cui si può scrivere del Software di alta qualità. Ma i risultati migliori (perlomeno in termini di prestazioni e fluidità del codice) si ottengono usandolo per quello che era stato concepito originalmente, e cioè come un C a oggetti. La piega che ha preso in seguito (da quando è caduto nelle mani dei committee ISO) non mi piace e non mi convince... ma, fortunatamente (e questo è importante), continua a poter essere usato nella sua essenza, quella che permette di scrivere a oggetti usando un linguaggio (quasi) identico al C (e questo si aggancia alla buona notizia qui sopra. Ovviamente, se prestazioni e fluidità del codice non si considerano qualità importanti, allora tutte queste considerazioni perdono di significato...).
Ah, una ultima precisazione per chi si è sorpreso del codice C++ (qui sopra) che include un VLA: è una gentile offerta dal nostro amato GCC (nella sua incarnazione G++). Quindi è un estensione del linguaggio fornita dal compilatore, visto che i VLAs non fanno parte del C++ standard (neanche nelle ultime versioni C++11 e C++14).
Nel prossimo post, per chiudere il cerchio, parleremo di un parente stretto dei VLAs, e cioè della funzione alloca(). Sarà un altro buono, un altro brutto o un altro cattivo?
Ciao e al prossimo post!
Nella versione ottimizzata il compilatore rimuove il codice inutile (riempire un vettore per poi buttarlo...). E infatti nel caso heapVLA (che accede memoria fuori dalla funzione) non rimuove il codice. Non se ne accorge nemmeno nel caso del Vector.
RispondiEliminaI tempi del vettore piu' lunghi sono dovuti al fatto che quando lo ridimensione
la memoria viene inizializzata col costruttore di default.
Se sposti il vettore fuori dalla funzione (come per heapVLA) i tempi sono identici, (l'ASM e' identico).
Grazie per la puntualizzazione. Hai ragione sulle ottimizzazioni: mi sono dimenticato di aggiungere almeno una istruzione (che usi l'array) nelle funzioni per evitare che l'ottimizzatore azzeri completamente il contenuto della funzione, visto che l'array creato non lo usa nessuno. Comunque, i risultati della versione non ottimizzata sono più che sufficienti (e significativi) per farsi un idea di chi è più o meno veloce.
EliminaModificherò in parte questo post e il precedente per eliminare l'errore. Ovviamente ti citerò e ringrazierò nel testo per la segnalazione.
Per quanto riguarda std::vector il discorso è: l'argomento del post sono i VLAs, mentre i FLAs sono stati aggiunti solo per avere un riferimento. Il std::vector fuori dalla funzione che proponi tu sarebbe un FLA molto simile (anche nelle prestazioni) al heapFLA. Ma in questo test std::vector si propone come alternativa a VLA e mallocVLA, e deve essere usato nello stesso modo, quello proposto. E le prestazioni sono sempre decisamente le peggiori del lotto (anche nella versione esterna alla funzione che dici tu, che ho provato).