I linguaggi di programmazione per il web sono molti, pertanto capita spesso di chiedersi quale strumento sia meglio usare prima di sporcarsi le mani in un nuovo progetto. Non esiste una risposta semplice a questa domanda perché le prestazioni dipendono da molti fattori. Per darmi una prima risposta, che non pretende di essere esaustiva, ho voluto effettuare una comparativa tra 5 dei più diffusi per il web: PHP, JavaScript, Ruby, Java e C++ (ok, ho imbrogliato questo non è così diffuso per il web ma è un ottimo riferimento per questo benchmark).
Quanti di voi conoscono la torre di Hanoi?
La Torre di Hanoi è un rompicapo costituito da tre paletti e un numero di dischi di grandezza via via decrescente impilati su uno di questi paletti. Obiettivo del rompicapo è spostare tutti i dischi da una pila ad un altra potendo spostare soltanto un disco alla volta e potendo solo spostare un disco più piccolo al di sopra di uno più grande, mai viceversa.
Dietro al gioco esiste una dimostrazione matematica che esplicita il numero minimo di mosse necessarie alla risoluzione del gioco e, assieme a questa anche un algoritmo per la risoluzione del gioco: se n è il numero di dischi, il numero minimo di mosse per completare il rompicapo è 2^n-1.
Il test
Ho pubblicato su GitHub un repository contenente l’algoritmo di risoluzione nei 5 linguaggi (PHP, JavaScript, Ruby, Java e C++) e uno script bash che effettua la compilazione dei sorgenti e l’esecuzione del codice nei diversi linguaggi.
Il codice è stato realizzato sfruttando il più possibile le strutture dati preesistenti dei vari linguaggi: ad esempio per C++ è stata usata la struttura stack presente nella Standard Template Library, array associativi nel caso di PHP e il classico oggetto Array di JavaScript.
Per la natura del problema il codice di risoluzione è stato implementato in tutti i linguaggi come un algoritmo ricorsivo. Riporto a titolo esemplificativo il codice nella versione Javascript, gli altri sono analoghi e sono consultabili sul repository GitHub.
var s1 = [];
var s2 = [];
var s3 = [];
var movesCount = 0;
Array.prototype.top = function() {
return this[this.length - 1];
};
Array.prototype.isEmpty = function() {
return this.length === 0;
};
function printStack(currStack) {
var temp = [];
var str = "";
var oss = "";
while (!currStack.isEmpty()) {
temp.push(currStack.pop());
}
while (!temp.isEmpty()) {
currStack.push(temp.top());
oss += temp.top() + " ";
temp.pop();
}
return oss;
}
function hanoi(source, dest, swap, depth) {
if (depth == 1) {
dest.push(source.top());
source.pop();
movesCount++;
return;
}
// spostare la torre depth-1 strati preesistente in swap
hanoi(source, swap, dest, depth - 1);
// fare la seguente mossa
dest.push(source.top());
source.pop();
movesCount++;
// ricreare la torre di depth-1 strati sopra la destinazione
hanoi(swap, dest, source, depth - 1);
}
function emptyStack(stack) {
while (!stack.isEmpty()) {
stack.pop();
}
}
function playGame(decks, quiet) {
s1 = [];
s2 = [];
s3 = [];
emptyStack(s1);
emptyStack(s2);
emptyStack(s3);
movesCount = 0;
for (i = 0; i < decks; ++i) {
s1.push(i);
}
if (!quiet) {
console.log("Inizio");
console.log("Stack1: " + printStack(s1));
console.log("Stack2: " + printStack(s2));
console.log("Stack3: " + printStack(s3));
}
var startTime = +new Date();
hanoi(s1, s3, s2, decks);
var finishTime = +new Date();
if (!quiet) {
console.log("Fine");
console.log("Stack1: " + printStack(s1));
console.log("Stack2: " + printStack(s2));
console.log("Stack3: " + printStack(s3));
}
var totalTime = (finishTime - startTime) / 1000;
console.log("decks:\t" + decks + "\ttime:\t" + totalTime + "\n");
}
// MAIN
console.log("Entry point");
var decks = parseInt(process.argv[2]);
playGame(decks, true);
I risultati del test
Il test è stato eseguito su un iMac mid 2010 (OSX 10.11.1) con le seguenti versioni di compilatori o interpreti:
- php 5.5.29
- node v0.12.0
- ruby 2.0.0p645
- javac 1.7.0_71
- g++ LLVM 7.0.0
Prima di trarre qualsiasi conclusione è bene ricordare che il test prende in considerazione soltanto alcune funzionalità dei linguaggi :
- allocazione di variabili e oggetti
- allocazione della memoria stack
- Passaggio di parametri per riferimento
che sono tutte operazioni sostanzialmente in carico alla CPU e memoria, quindi non stiamo considerando come i linguaggi si comportano su altri tipi di operazioni come ad esempio I/O di grandi quantitativi di dati su memoria o disco. Inoltre i tempi di esecuzione sono calcolati come il tempo che intercorre tra la prima e l’ultima mossa del rompicapo. Sono quindi esclusi tutti i tempi di overhead di caricamento dello script e interpretazione del codice.
Qui di seguito riporto i tempi di esecuzione dell’algoritmo espressi in secondi, al variare del numero di dischi tra 18 e 24.
Num. Dischi | C++ | Javascript | Java | Ruby | PHP |
18 | 0.0020 | 0.0040 | 0.0480 | 0.0632 | 0.2907 |
19 | 0.0050 | 0.0070 | 0.0570 | 0.1253 | 0.5959 |
20 | 0.0063 | 0.0083 | 0.0740 | 0.2385 | 1.1870 |
21 | 0.0190 | 0.0290 | 0.1130 | 0.5033 | 2.5338 |
22 | 0.0330 | 0.0430 | 0.1690 | 0.9547 | 4.8684 |
23 | 0.0760 | 0.0860 | 0.3060 | 2.0705 | 10.1063 |
24 | 0.1260 | 0.1690 | 0.5710 | 3.8711 | 20.3050 |
Dai dati e dal grafico si legge chiaramente la complessità asintotica O(2^n) dell’algoritmo. Ciò che emerge di interessante sono le ottime prestazioni dei linguaggi C++ (ma questo era facilmente intuibile), Java e Javascript.
Eliminando dal grafico le serie relative a Ruby e PHP, osserviamo che Javascript si attesta a livelli di prestazioni molto simili a quelli di C++, cosa non scontata, data la diversa natura di linguaggio interpretato del primo e compilato del secondo, a prova dell’ottimo lavoro fatto nello sviluppo e ottimizzazione del motore V8 che sta alla base di Node.js
Linguaggi molto diffusi come Ruby e soprattutto PHP soffrono parecchio il confronto: già con 22 dischi Ruby impiega quasi 1 secondo per portare a termine l’algoritmo, PHP quasi 5 secondi, mentre Javascript si attesta a circa 170 millisecondi.