![]() |
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!