Evolution des performances des versions de ruby

Publié le 5 décembre 2014 par Jonathan François | dev

Cet article est publié sous licence CC BY-NC-SA

Ruby a cette année 21 ans. Créé par Matsumoto en 1993, le langage a connu de multiples changements et évolutions depuis sa première publication officielle en 1995.

Depuis quelques années et surtout avec l’arrivée du framework Ruby on Rails, Ruby est devenu de plus en plus populaire. D’après l’index Tiobe qui mesure la croissance des langages informatiques, Ruby est particulièrement stable puisqu’il n’a bougé que de 0.09% par rapport à l’année dernière.

C’est intéressant de jeter un œil à la courbe d’utilisation de 2002 à aujourd’hui.

Contexte

Depuis la sortie du premier microprocesseur en 1971 par Intel, Intel 4004, les choses ont bien changées. À l’époque l’un des fondateurs d’Intel, Gordon Moore avait émis une série de lois empiriques (Lois de Moore) permettant d’estimer l’évolution de la puissance des ordinateurs en fonction du temps.

La prédiction mentionnait une multiplication par 2 de la performance des microprocesseurs tous les 18 mois. Cette loi n’a rien de scientifique mais a pourtant servi au lancement de grandes sociétés comme Microsoft, Apple… pour estimer le coût et les performances de leurs futures produits.

Nous sommes passés de l’intel 4004 à 740 KHz en 1971 kHz à l’Intel Core i7 à 4,00 Ghz en 2013.

Concernant l’évolution de la mémoire vive nous sommes passés de 4Bytes (Intel 4004) à aujourd’hui plus de 32GB.

La question qu’on va se poser est : les nouvelles versions de nos langages informatiques, en l’occurrence ici Ruby, s’efforcent-elles d’être de moins en moins gourmandes en terme de temps d’exécution et de consommation mémoire malgré l’évolution exponentielle de nos machines ?

Versions testées

En s’intéressant aux différentes versions publiées ces dernières années, nous avons voulu quantifier l’évolution en terme de performances.

Nous allons donc réaliser des benchmarks de différentes méthodes utilisées depuis le début du langage en fonction de différentes versions de ruby.

Voici la listes des versions ruby concernées et leur date de sortie:

  • 1.8.6-p383 - 2003
  • 1.9.3-p0 - 2007
  • 2.0.0-p0 - 2013
  • 2.1.5 - 2014

Code testés

Pour simplifier le code présenté, nous utiliserons la class Bench pour représenter le benchmark. POur plus de détails sur la mise en place d’un benchmark vous pouvez vous référer à notre article de benchmarking d’activerecord.

Voici la liste des fonctions que l’on va tester:

L’incontournable méthode push :

    Bench.run [1000000] do |n|
      a = []
      n.times { a.push([]) }
    end

Méthode eval :

    Bench.run [1000000] do |n|
      n.times { eval "a = 33" }
    end

Méthode open pour un fichier :

random.input étant un fichier de 10 000 lignes comportant un nombre par ligne.

    fname = File.dirname(__FILE__) + "/random.input"
    Bench.run [50000] do |n|
      n.times {
        f = File.open(fname, "r")
        f.close
      }
    end

Fonction de regexp :

Recherchons dans ce fichier (fasta.input) les caractères correspondant à une chaîne ADN et comptons le nombre de chaîne identique.

    Bench.run [20] do |n|
      n.times do
        fname = File.dirname(__FILE__) + "/fasta.input"
        seq = File.read(fname)
        ilen = seq.size

        seq.gsub!(/>.*\n|\n/,"")
        clen = seq.length

        [
        /agggtaaa|tttaccct/i,
        /[cgt]gggtaaa|tttaccc[acg]/i,
        /a[act]ggtaaa|tttacc[agt]t/i,
        /ag[act]gtaaa|tttac[agt]ct/i,
        /agg[act]taaa|ttta[agt]cct/i,
        /aggg[acg]aaa|ttt[cgt]ccct/i,
        /agggt[cgt]aa|tt[acg]accct/i,
        /agggta[cgt]a|t[acg]taccct/i,
        /agggtaa[cgt]|[acg]ttaccct/i
        ].each {|f| puts "#{f.source} #{seq.scan(f).size}" }

        {
        'B' => '(c|g|t)', 'D' => '(a|g|t)', 'H' => '(a|c|t)', 'K' => '(g|t)',
        'M' => '(a|c)', 'N' => '(a|c|g|t)', 'R' => '(a|g)', 'S' => '(c|t)',
        'V' => '(a|c|g)', 'W' => '(a|t)', 'Y' => '(c|t)'
        }.each { |f,r| seq.gsub!(f,r) }
      end
    end

Cet example est l’un des benchmarks utilisés par le site http://benchmarksgame.alioth.debian.org/.

Résultats des benchmarks

Les benchmarks ont été réalisés avec 5 itérations chacun, nous présenterons ici une moyenne.

Voici le tableau correspondant au temps d’exécution (ms) en fonction des versions de Ruby :

Results_by_time

et son graphique :

Results_by_time_grap

A l’exception de la méthode eval, le temps d’exécution tend à se réduire. Il faudrait bien sûr tester la totalité des méthodes Ruby pour avoir une vision globale. Ici nous nous posons juste la question et ne testons que quelques méthodes.

Ces différences proviennent de la modification de l’implémentation des méthodes et pour cela il faut se pencher sur le code en question.

Vous pouvez retrouver le code de l’implémentation ici pour la version 1.8.6 et ici pour la 2.1.5. En examinant le code des deux versions, il se pourrait que cette différence proviennent de l’utilisation de eval_string ou de SafeStringValue.

D’ailleurs pour comprendre la différence en terme de performance et de temps d’exécution de chacune des méthodes testées ici, il faudrait systématiquement se référer à son implémentation pour en comprendre l’origine.

Ce point pourrait faire l’objet d’une série d’article à lui seul.

Voici le tableau concernant le consommation mémoire en Bytes :

Results_by_meter_memory

et son graphique :

Results_by_meter_memory

Concernant la consommation mémoire, il y a une tendance à la hausse pour la totalité des méthodes testés. À noter que nous utilisons les versions MRI de base sans chercher à faire quelques réglages que ce soit sur la VM ruby (YARV).

Entre 2003 (date de sortie de la version 1.8.6) et 2014 (celle de la 2.1.5), sur les 4 méthodes testées la consommation mémoire moyenne est passée de 33MB à 38MB soit 15% d’augmentation. Si l’on compare sur la même période l’évolution de la capacité de mémoire vive et puissance de calcul de nos machines cela reste ridicule.

Ces 4 exemples ne suffisent en rien à pouvoir dégager une conclusion correcte à la question posée en début d’article, mais nous permet d’en déduire deux choses :

  • de manière générale les nouvelles versions de ruby semblent légèrement plus gourmandes que leur prédécesseuses
  • certaines implémentation de méthodes deviennent de plus en plus longue à l’exécution (dans notre cas nous n’avons que la méthode eval)

Il serait intéressant de réaliser ces tests sur l’ensemble des méthodes Ruby pour avoir une vision plus complète et permettre de répondre totalement à la question.

Alors si le cœur vous en dit et que vous débordez de courage en cette fin d’année, vous pouvez vous tourner vers ruby-benchmark-suite qui pourra vous aider à réaliser ces tests.

Avec un peu d’anticipation, je vous souhaite de bonnes fêtes de fin d’année.

L’équipe Synbioz.

Libres d’être ensemble.