Programmazione 7 commenti
Ruby contro Ruby
diDue 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:
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:
- Ruby 1.8.6 (stabile, patchlevel 111)
- Ruby 1.9.0 (in sviluppo, 02/12/2007)
- Rubinius 0.8.0 (in sviluppo, 02/12/2007)
- JRuby (in sviluppo, 02/12/2007)
- XRuby 0.3.2 (stabile)
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.
- bm_app_answer.rb
- bm_app_factorial.rb
- bm_app_fib.rb
- bm_app_mandelbrot.rb
- bm_app_pentomino.rb
- bm_app_raise.rb
- bm_app_strconcat.rb
- bm_app_tak.rb
- bm_app_tarai.rb
- bm_loop_times.rb
- bm_loop_whileloop.rb
- bm_loop_whileloop2.rb
- bm_so_ackermann.rb
- bm_so_array.rb
- bm_so_concatenate.rb
- bm_so_count_words.rb
- bm_so_exception.rb
- bm_so_lists.rb
- bm_so_matrix.rb
- bm_so_nested_loop.rb
- bm_so_object.rb
- bm_so_random.rb
- bm_so_sieve.rb
- bm_vm1_block.rb
- bm_vm1_const.rb
- bm_vm1_ensure.rb
- bm_vm1_length.rb
- bm_vm1_rescue.rb
- bm_vm1_simplereturn.rb
- bm_vm1_swap.rb
- bm_vm2_array.rb
- bm_vm2_method.rb
- bm_vm2_poly_method.rb
- bm_vm2_poly_method_ov.rb
- bm_vm2_proc.rb
- bm_vm2_regexp.rb
- bm_vm2_send.rb
- bm_vm2_super.rb
- bm_vm2_unif1.rb
- bm_vm2_zsuper.rb
- bm_vm3_thread_create_join.rb
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.
Figura 2: Risultati 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.
Figura 4: Media geometrica su Linux
Figura 5: Grafico delle medie su Linux
Per gli interessati a Ruby.NET, ecco i risultati su Windows:
Figura 6: Risultati totali 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.
- Pubblicato il
- 15 Dic 2007
- Tag


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.
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.
Articolo mooooolto interessante! Non vedo l'ora che esca Ruby 1.9! :)
Grazie ragazzi! Mancano solo pochi giorni all'uscita di Ruby 1.9, l'attesa è quasi finita. :)
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.
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!
@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. :)