6 settembre 2009

benchmark

Prima dell'estate ho letto un sacco di benchmark di paragone fra vari linguaggi di programmazione.
Cercavo, spunti, conferme e dati interessanti che mi permettessero di avere una valutazione prestazionale di varie tecnologie, che comunque rimane solo uno dei mille aspetti che determinano una scelta tecnologica rispetto ad altre.

Sono sempre stato un appassionato di classifiche, quasi un feticista delle classifiche. Delle scarpe col tacco o della lingerie no, ma delle classifiche sì, quindi in prima posizione la lingerie e in seconda i tacchi. Quindi i benchmark mi appassionano anche perché hanno sempre questo aspetto di confronto che definisce, spesso, un sacco di classifiche, per dati semplici, poi per aggregazione di dati non omogenei in partenza che vengono pesati...un'orgia di classifiche, insomma. Ma questa è un'altra storia.

Ho trovato un sacco di pane per i miei denti in rete. Ho letto ed elaborato informazioni. Poi sono stato assalito da dubbi. Ho cercato dove possibile di capire la natura delle prove che costituivano i benchmark in questione. I più interessanti si trovano qui: Computer Language Benchmark Game.

Sono tutti o quasi benchmark di problemi dalla soluzione nota risolubili con un algoritmo computazionale più o meno pesante, quasi sempre su interi o numeri in virgola mobile. Qualche eccezione prevede operazioni su stringhe e espressioni regolari triviali.

Mi ha colpito come su decine di prove si verifichino sempre le stesse cose, creando sproporzioni artificiose in favore di alcuni e contro altri: chi risolve bene i problemi di una categoria risolve bene, con minime varianti, i problemi della stessa categoria che presentano solo piccole varianti sul problema originale. Ergo chi vince sul primo vince su tutti, cioè piazza invece che un 1 a 0 un 10 a 0. Poi magari di un'altra categoria si pone un solo problema, in cui il primo linguaggio perde in favore del secondo, ma non esistendo varianti nel benchmark il risultato è 10 a 1 e non un 1 pari come forse sarebbe stato più giusto...

Vabbeh. Poco male. Le classifiche sono associazioni di dati, quando si vuole il dettaglio bisogna scavare.

E scavando appare anche evidente che l'obiettivo sembra quello di porre problemi relativamente facili da rappresentare, ma questo, seppure in senso lato, è un altro modo di categorizzare il mondo. Chi fa della flessibilità o della capacità di modellare situazioni il suo punto di forza perde magari tutti i confronti sui casi ben definiti, ma vince in quelli più difficili.

Per ora tutti discorsi generici, entro nello specifico, prendendo 4 linguaggi di cui ho una conoscenza di base sufficiente per poterne parlare con cognizione di causa: C, C++ (e chi dice C/C++ probabilmente è perché il C++ ha mantenuto la backward compatibility e crede quindi che sia una estensione con le stringhe nella STL del C), Python 2.x e Java 6. Li abbrevierò in C, C++, Py e J6.

Che in problemi strettamente computazionali il C batta Py e J6 è così evidente che serve a poco scriverlo...ma è comunque interessante sapere di quante lunghezze. Sul confronto C vs C++ si dovrebbero spendere troppe parole, fatto sta che i 2 per problemi del genere possono risolvere il problema in modo così simile da risultare quasi indistinguibili.

J6 straccia Py, merito soprattutto dell'ottimo JIT di Java e dell'esclusione di Psyco dai benchmark di Py (che farebbe scendere il rapporto da x30 a circa x1.3, che insomma, ha il suo peso).

I valori medi dei 4 linguaggi per tutti i benchmark su grossi numeri è:
C : 1.01
C++ : 1.00
J6 : 1.68
Py :54.03

Dove 1 è il valore del migliore, il resto in rapporto, credo.

Cosa emerge cercando meglio? che sono quasi tutti dei mega loop con operazioni matematiche semplici, ossia cose che se dici a un compilatore C di sbrogliare i loop, lui bello bello ti cabla la ripetizione delle operazioni nel programma, facendotelo diventare sì un tonno, ma togliendo pure quell'overhead dei jump a fine ciclo. Che se hai un JIT o simili il peso del JIT scompare. Che se sei un interpretato interpreterai 10mila volte 1+1...

Ora vediamo nello specifico che invece, se si prende il test regex-dna, che ha un po' di smanettamento sulle stringhe, si ottiene questa classifica, questa volta in secondi di esecuzione:

C++ : 19sec (ma bara, usando un botto di librerie di terze parti, e anche al soluzione da 26 secondi usa una libreria di espressioni regolari non standard)
Py : 29sec
j6 : 31sec (in server edition, per questo test)
C : 32sec

Questo perché? come può C perdere contro Py?
Diamo una risposta di base un po' strana: l'interprete Python è scritto in C, quindi formalmente un programma Python potrebbe essere considerato come un file di configurazione un programma C incredibilmente complesso. Quindi Py non batte realmente C. Il problema è che bisogna trovare un soluzione che tenga conto anche delle dimensioni e non solo delle prestazioni. Cioè non posso scrivere una applicazione da alcuni mega per uno stupido programma Py da 20 righe circa...
La realtà è che quindi il modulo delle espressioni regolari in Py lavora meglio di quanto possa fare uno bravo che scrive una soluzione specifica al problema senza voler costruire un missile nucleare per uccidere una zanzara.

Ora andiamo avanti con il ragionamento, ma spostiamoci nell'astratto: cosa succederebbe se a dover essere rappresentato fosse un problema abbastanza complesso da spingere un programmatore medio a scrivere una gerarchia di classi?
Il C++ lo affronterebbe benissimo, allo stesso modo J6 e Py, almeno da un punto di vista della rappresentazione della soluzione. In C si comincerebbe a chiedersi se il problema abbia complessità tale da imbarcarsi in una serie di struct di puntatori a funzioni, ossia della cosa che più si avvicina a una classe, senza però tutte le facility del C++, senza costruttori e distruttori, con meccanismi di ereditarietà tutti da inventare....

E se il problema fosse abbastanza complesso da spingere un programmatore C++ o Py a scrivere un programma strutturato con ereditarietà multipla? In J6 te la cavi o tramutando il canonico rapporto is-a con un has-a poi ridefinendo tutto il flusso della soluzione, o duplicando tonnellate di codice o smanettando con pratiche non molto comuni. In C ti metti le mani nei capelli e poi ti rassegni: comincia a costruire un castello immenso che richiede analisi approfondita del caso in questione...oppure abbandoni e lo fai in C++ :)

Però se un problema arriva a quel livello di complessità, sicuramente esistono più soluzioni plausibili per affrontarlo e quindi le soluzioni proposte non sempre sono pienamente paragonabili. Peggio, quelle più preformanti sono probabilmente quelle che prescindono da una buona rappresentazione del problema e vanno dritte al nocciolo, con buona pace della manutenibilità del codice e, peggio, della sua estensibilità.

Purtroppo banchmark su problemi di questo tipo è veramente dura trovarne, quindi ci si rassegna a un po' di speculazioni, che riguardano la bontà di tenersi una tabella di riferimenti ai metodi come fanno C++ e Py contro la ricerca in risalita lungo la gerarchia come fa (o faceva, su questo punto sono rimasto indietro, ma non credo abbiano cambiato) Java.

Poi c'è la questione che Java arriva insieme a librerie per fare di tutto di più, Python un po' meno, C++ mostruosamente meno, C è un disastro. Quindi quello che è un problema banale in un linguaggio in un altro potrebbe essere fonte di non pochi grattacapi, anche se poi, la soluzione specifica al problema potrebbe essere più performante.

Insomma, mi hanno detto in mille modi che una ferrari va più veloce di una persona, ma non mi hanno detto che la persona poteva tagliare per un vicoletto di 15 metri e la ferrari ha dovuto fare un giro di 15 km con 4 semafori per arrivare allo stesso punto.

Resta il fatto che se uno deve fare un loop in cui fa 10000 mila volte una somma e lo deve fare in modo performante, se sceglie un linguaggio interpretato è probabilmente un imbecille...
Read more