Ruby contro Ruby

Due anni fa circa, dopo essere stato assunto dai laboratori di ricerca IBM
(per l’esattezza nel Toronto Software Lab), ricordo di aver presentato in un
meeting i pregi del linguaggio di programmazione Ruby a un gruppo di persone
influenti nell’azienda. Dopo avermi ascoltato in un silenzio quasi religioso,
il primo commento arrivò da un Distinguished Engineer che mi disse:
«Interesting but… hmmm, it sounds slow». La sua poteva essere deformazione
professionale, visto che è specializzato nel campo dell’ottimizzazione delle
prestazioni di sistemi informatici; ma come tutti i programmatori Ruby sanno,
la sua affermazione rimane alquanto attuale ed accurata.

Ruby è lento. Le caratteristiche di un linguaggio influiscono sulle
prestazioni delle applicazioni scritte nello stesso, ma è soprattutto
l’implementazione del compilatore, dell’interprete o della virtual machine a
definirne la velocità. Ad essere più precisi perciò Ruby non è lento, ma è la
sua implementazione principale (Main Ruby Interpreter o MRI) ad esserlo. Nel
linguaggio comune, la distinzione è spesso omessa in favore della semplicità
espressiva ma è importante capire sin da subito che una differenza esiste.
Accettando dunque l’identificazione della data implementazione con il
linguaggio stesso, possiamo sostenere che Ruby è lento. Ma lento rispetto a
cosa?

Sarebbe facile cadere nel relativismo del “tutto dipende”, spesso
adottato da alcuni nella difesa miope del proprio linguaggio preferito. In un
qualsiasi test, un programma scritto puramente in Ruby sarà sempre più lento
di uno scritto in C o C++, a parità di algoritmo. Questo non sorprende, visto
che stiamo confrontando linguaggi notoriamente veloci, statici e compilati,
con uno dinamico e interpretato. Il problema viene evidenziato quando si
confronta Ruby con linguaggi d’alto livello simili, come Perl, Python e Lua.
Quest’ultimi sono anch’essi necessariamente più lenti del C, ma garantiscono
prestazioni più accettabili di quelle di Ruby 1.8.6.

L’evidenza aneddotica di chi programma in due di questi linguaggi è
schiacciante, ma se questo non fosse sufficiente a dimostrare la tesi che Ruby
offre prestazioni meno che ottimali, ci basta osservare i risultati del Computer
Language Benchmark Game
per verificare che Ruby, per quanto elegante e
semplice da imparare, ha delle difficoltà evidenti in termini di velocità di
esecuzione del codice. Stando ai risultati di quel test, Ruby è lento rispetto
a (praticamente) ogni altro linguaggio presente. I micro-benchmark
sono notoriamente poco accurati o rappresentativi delle prestazioni di
programmi reali, ma anche prendendo le dovute precauzioni nella lettura dei
risultati, produrre consistentemente risultati inferiori ad altri linguaggi è
indicativo di una carenza concreta.

La lentezza di Ruby è diventata il suo tallone d’Achille e spunta fuori
ogni qualvolta si discute del linguaggio. C’è chi la usa per bilanciare le
promesse molto ottimistiche di un linguaggio ben disegnato e chi, favorendo un
altro linguaggio, la impugna per discreditare Ruby nella sua interezza. Ciò
che conta maggiormente sono i due tipi di difesa che si possono osservare
nella (quasi fanatica) comunità Ruby internazionale. Il primo approccio è
quello di negare. Esistono diversi individui che apertamente negano la
lentezza di Ruby, osservando che è possibile utilizzare C all’interno di un
programma scritto in Ruby, risolvendo in questo modo i possibili colli di
bottiglia dovuti alla lentezza del linguaggio. Queste persone sono le meno
oneste intellettualmente e tendono ad attaccare i test di cui sopra o
l’inefficienza degli algoritmi usati dal programmatore medio.

L’integrazione
di spezzoni di codice C all’interno di programmi in Ruby è una funzionalità
molto utile ed è vero che tutti i programmatori dovrebbero scrivere codice
comprendendo il concetto di complessità algoritmica. Aggiungo personalmente
che la velocità dell’implementazione di un dato linguaggio è solo una delle
caratteristiche desiderabili e spesso anche Ruby (nella sua lentezza) è più
che adeguato. Tutto ciò non giustifica però il fatto che Ruby debba rimanere
così lento, quando altri linguaggi simili sono più efficienti. Il secondo
approccio, che apprezzo maggiormente, è quello di accettare il fatto che
Yukihiro Matsumoto (noto come Matz) e il suo team, abbiano preferito dedicarsi
ad altri aspetti del linguaggio a scapito dell’ottimizzazione dell’interprete.
Tra le fila di questo gruppo ci sono coloro che aspettano con ansia nuove
versioni più efficienti e coloro che hanno deciso di scendere in campo
direttamente creando la loro implementazione di Ruby.

Tra le implementazioni alternative più prominenti figurano Yarv
(incorporato in Ruby 1.9 che uscirà a Natale), JRuby promosso da Sun, XRuby,
Ruby.NET e Rubinius. Con tutte queste “versioni” di Ruby disponibili, dieci
mesi fa decisi di metterne alla prova alcune in modo tale di avere un’idea chiara dei
rispettivi progressi nei confronti dell’implementazione standard (che allora
era la 1.8.5). Sul banco di prova, misi Ruby 1.8.5, Ruby 1.9, JRuby, Ruby.NET,
Rubinius e Cardinal (Ruby per Parrot). La tabella seguente mostra i risultati
(espressi in secondi) ottenuti a Febbraio:

Risultati a Febbraio 2007

Figura 1: Risultati a Febbraio 2007

Noterete che nella maggior parte dei test, che costituiscono un set
standard nel mondo Ruby, Yarv (Ruby 1.9) era l’unico più veloce di Ruby 1.8.5.
Nonostante ciò, a suo tempo ho definito i risultati incoraggianti, perché
ognuno di questi progetti era solamente agli albori e non era stato
ottimizzato in alcun modo.

A distanza di 10 mesi, ho deciso di confrontare, in quello che ho chiamato
The
Great Ruby Shootout
, le principali implementazioni di Ruby per misurarne i
progressi e fare il punto della situazione. Su Linux (con Ubuntu 7.10 per x86)
ho fatto girare i benchmark per le seguenti implementazioni:

JRuby e XRuby, basati entrambi sulla Java Virtual Machine (JVM), sono stati
compilati da codice con JDK 6 Update 3. I test sono poi stati fatti girare
impostando 4 Megabyte di stack per entrambi. JRuby ha dimostrato le
prestazioni migliori passando l’opzione -J-server che rende la JVM più lenta
in fase di startup, ma più veloce in fase di esecuzione del bytecode.
Il tempo di startup non è stato ovviamente preso in considerazione per JRuby,
XRuby o alcun’altra implementazione, perché irrilevante nella maggior parte
dei casi per applicazioni reali. Questo significa anche che i tempi registrati
per Rubinius, sono calcolati solo successivamente alla creazione del proprio
bytecode nei file .rbc. L’hardware per i test su Linux è un AMD Athlon™
64 3500+ con 1 GB di RAM.

Ruby.NET con Mono su Linux è praticamente tagliato
fuori per via degli errori e i timeout (lo script è stato interrotto
manualmente) generati in quasi metà dei test. Per dare una chance a
quest’ultimo, Ruby 1.8.6 e Ruby.NET 0.9 sul .NET Framework 2.0 sono stati
fatti girare anche su Windows XP SP2 su un Intel Core Duo L2400 1.66GHz con 2
GB di RAM. Si faccia attenzione a non confrontare i due risultati (Linux e
Windows) visto che l’hardware è chiaramente diverso. I due vanno intesi come
test del tutto separati. Si eviti anche di confrontare i risultati di dieci
mesi fa su Windows con quelli recenti, perché l’hardware per Windows è
cambiato tra i due test.

Cardinal è stato escluso dal test, perché
ho deciso di includere solamente implementazioni prominenti. IronRuby invece non è stato
incluso perché ancora in pre-alpha. Per i più curiosi, IronRuby e Ruby.NET
sono simili, dato che entrambi puntano a generare assembly che girino sul
Microsoft .NET Framework e su Mono. Le tre differenze principali sono la
maturità dei progetti (Ruby.NET è quasi pronto per la versione 1.0 e per
eseguire Rails), il fatto che IronRuby è sviluppato direttamente da Microsoft
(ma entrambi sono open source) e infine, la differente filosofia che li porta
ad adottare rispettivamente il CLR (Common Language Runtime) per Ruby.NET e il
nuovissimo DLR (Dynamic Language Runtime) per IronRuby.

Per questo aggiornamento del confronto tra implementazioni di Ruby, ho
deciso di includere 43 test. I 41 standard, usati in precedenza ed elencati di
seguito, il test della matrice di Ed Borasky (con n = 50) e una versione
semplificata di bm_app_factorial (chiamata bm_easier_fact), identica alla
prima ma con n = 4000 per verificare la velocità di esecuzione da parte delle
implementazioni non in grado di eseguire il test per n = 5000.

La tabella seguente mostra i tempi d’esecuzione espressi in secondi, su
Linux. I valori in verde indicano una prestazione migliore di quella di MRI
(Ruby 1.8.6), mentre lo sfondo giallo canarino indica che l’implementazione è
stata la più veloce del gruppo per il dato test. I totali a fondo tabella sono
calcolati come somma dei test eseguiti con successo da parte di tutte le
implementazioni partecipanti al test.

Risultati su Linux

Figura 2: Risultati su Linux

Grafico totali su Linux

Figura 3: Grafico risultati totali su Linux

La seguente tabella mostra il rapporto tra i valori di riferimento per Ruby
1.8.6 e i valori di ogni singola implementazione. Perciò 2.0 indica che la
virtual machine è stata due volte più veloce, mentre 0.5 che è stata due volte
più lenta. La media geometrica (semplice) calcolata in fondo alla tabella, ci
fornisce un’indicazione di quanto ogni candidato sotto test è più veloce o
lento dell’interprete corrente. Anche qui, come per i valori totali, il
calcolo è stato effettuato solamente per i test eseguiti con successo da tutte
le virtual machine e nel caso di app_factorial è stato preso il valore di Ruby
1.8.5 perché Ruby 1.8.6 ha generato un’eccezione.

Media geometrica su Linux

Figura 4: Media geometrica su Linux

Grafico medie su Linux

Figura 5: Grafico delle medie su Linux

Per gli interessati a Ruby.NET, ecco i risultati su Windows:

Risultati su windows

Figura 6: Risultati totali su Windows

Media geometrica su Windows

Figura 7: Media geometrica su Windows

I valori del benchmark corrente sono disponibili anche in formato Excel e PDF.

Passiamo subito all’analisi dei risultati. La prima considerazione che
appare ovvia, è il netto miglioramento da parte di tutte le implementazioni
che sono state provate in precedenza (in pratica, tutte tranne Cardinal e
XRuby che non erano presenti in entrambi i test). JRuby e Rubinius in
particolare hanno dimostrato un miglioramento eccezionale in meno di un anno e
fanno ben sperare per il futuro. JRuby è anche l’unica implementazione in
grado di passare tutti i test senza andare mai in errore o in timeout e
l’unica, assieme a Ruby 1.8.6, a far girare Ruby on Rails in produzione. Yarv
rimane la più veloce, circa tre volte di più dell’interprete corrente (su
questi test) come mostrato nel grafico delle medie su Linux, ma altre
implementazioni come la cinese XRuby e JRuby stesso, iniziano ad avvicinarsi.
Rubinius che nel primo test era tremendamente lento e prono all’errore, riesce
ora a “battere” Ruby 1.8.6 in ben 16 test. Cosa accadrà tra un anno? Staremo a
vedere, ma la speranza di avere implementazioni Ruby efficienti cresce.

Ruby aveva un grosso problema e la comunità si è mobilitata per cercare di
risolverlo al più presto nel migliore dei modi. A Natale avremo già Ruby 1.9
che, si sospetta, avrà prestazioni confrontabili con quelle di CPython.
L’effetto collaterale (positivo) di questi tentativi di miglioramento per Ruby
1.8, è l’ampia scelta di opzioni che saranno disponibili presto per gli
sviluppatori Ruby e Rails. A parte Yarv o Rubinius, che utilizzano un bytecode
proprio, JRuby e XRuby permettono l’integrazione di codice Ruby in
un’infrastruttura basata su Java, mentre Ruby.NET e IronRuby fanno lo stesso
per il mondo .NET. Progetti come JRuby aprono dunque la porta a Ruby nel mondo
Enterprise, dove sostituire Java con Ruby è spesso impensabile, ma integrarlo
facendolo girare su JVM e utilizzano servizi e librerie Java preesistenti è un
compromesso che rende felici sia gli sviluppatori sia i manager. Per
concludere, si ricorda al lettore che la velocità di un linguaggio (o meglio
di una sua implementazione) diventa davvero importante solo per certe
applicazioni e se esiste un’effettiva carenza rispetto ad altri linguaggi come
nel caso di Ruby 1.8. Più in generale, ci sono molti aspetti che portano alla
scelta di un linguaggio e Ruby è spesso considerato uno dei migliori, sotto
diversi punti di vista. Una virtual machine veloce sarà dunque solo
un’aggiunta, attesa da tempo, a un linguaggio altrimenti molto apprezzato.

About Antonio Cangiano

Antonio lavora come Software Engineer & Technical Evangelist presso la IBM in Canada. È inoltre il direttore di Stacktrace.it, un internet marketing strategist, imprenditore del web, serial blogger, e autore di un paio di libri in inglese (recentemente Technical Blogging.) Puoi dare un'occhiata ai suoi progetti sulla sua homepage e seguendolo su Twitter.

Comments

  1. Sono molto contento dei risultati, devo dire che sono rimasto un po a bocca aperta la prima volta che ho visto le differenze di velocità in ruby 1.9. Anchio ho fatto un paio di test (banali) che ho postato sul mio blog, e devo dire che sembrerebbe che ci sia qualche problemino con la gestione delle stringhe. Comunque staremo a vedere che succede, senza magari perdersi tra le tante nuove versioni di ruby che ci saranno.

  2. Mirko Sciachero says:

    Molto interessante, c’è da notare come la versione 1.9 ha fatto un notevole passo in avanti rispetto alla 1.8.6.

    Per quanto riguarda JRuby (di cui non sono a conoscenza dei requisiti Java) immagino abbia beneficiato notevolmente anche dei miglioramenti di performance della JVM 1.6 rispetto alle precedenti, oltre che essere arrivato ad un livello di sviluppo più stabile rispetto ai test precedenti.

  3. Articolo mooooolto interessante! Non vedo l’ora che esca Ruby 1.9! 🙂

  4. Grazie ragazzi! Mancano solo pochi giorni all’uscita di Ruby 1.9, l’attesa è quasi finita. 🙂

  5. Purtroppo la lentezza di Ruby sembra essere il maggior difetto di questo linguaggio che a me piace moltissimo. Per fortuna i risultati della 1.9 sembrano confortanti.

  6. Solo un piccolo appunto: nell’implementazione di fibonacci c’è un errore ovvero se n<3 ritorna 1 il ch è un imprecisione perchè il fib(0) = 0.
    Comunque ottimo articolo!

  7. @Berna: Sì tratta di un errore noto, compiuto da chi ha scritto i benchmark standard per Yarv. I link puntano alla repository e quindi contengono ancora l’errore, ma avevo già corretto il test che ho fatto girare sulla mia macchina. Comuqnue, dal punto di vista perfomance questo non ha ovviamente alcun impatto. Grazie ancora per i complimenti. 🙂

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.