Sortie de Ruby 3.0

Posté par  . Édité par Ysabeau, Quidam, miko, palm123, Benoît Sibaud, Yves Bourguignon et gUI. Modéré par Ysabeau. Licence CC By‑SA.
Étiquettes :
46
30
août
2021
Ruby

Le 25 décembre 2020 le langage Ruby est passé en version 3.0 !

three

Image de Jack Hunter.

Cette version est le fruit de cinq ans de travail, le travail sur la branche 3.0 ayant commencé en 2015.

La suite de cette dépêche retrace les changements contenus dans cette nouvelle version.

Sommaire

Versions

La date du 25 décembre n’est pas un hasard puisque cela fait plusieurs années que les versions mineures sortent le 25 décembre.

Petit rappel des sorties de Ruby :

  • Ruby 1.0 est sorti le 25 décembre 1996
  • Ruby 2.0 le 24 février 2013
  • Ruby 2.1 le 25 décembre 2013
  • Ruby 2.2 le 25 décembre 2014
  • Ruby 2.5 le 25 décembre 2015
  • Ruby 2.4 le 25 décembre 2016
  • Ruby 2.5 le 25 décembre 2017
  • Ruby 2.6 le 25 décembre 2018
  • Ruby 2.7 le 25 décembre 2019

La liste quasi complète des versions de Ruby peut se trouver ici.

Liens vers d’anciennes dépêches LinuxFr.org

Quelques liens vers des dépêches LinuxFr.org autour de Ruby et son écosystème :

Performances

Une des principales améliorations de Ruby 3.0.0 concerne les performances. En effet, Ruby peut parfois exécuter votre code jusqu’à trois fois plus vite.

Ci-dessous les résultats d’un benchmark tiré de l'annonce officielle :
Résultats benchmark performances

L’accélération provient principalement de l’utilisation du JIT soit « Just-In-Time » compilation.

Rappel, pour celles et ceux qui ne connaissent pas ce procédé : un interpréteur produit généralement du « byte code » (sorte de code simplifié de plus haut niveau que l’assembleur mais en restant bas niveau, parfois au format binaire). Ce byte code est ensuite exécuté par une « machine virtuelle ». Dans certains cas la frontière entre interpréteur et machine virtuelle est très mince (Perl, Python avec CPython, Ruby avec CRuby < 1.9) ou plus marquée (Raku = Rakudo + MoarVM/JVM/JS, Ruby = Ruby + YARV, Erlang = Erlang + BEAM) même si cela ne change pas grand-chose d’un point de vue utilisateur (le code source semble exécuté par une seule et même entité).

Le processus de génération de bytecode peut se faire parfois ligne par ligne (interpréteur de Basic) mais peut également être précédé d’une étape de génération de format intermédiaire avec des optimisations (par exemple via un arbre de syntaxe abstraite). On nomme parfois cette étape une étape de « compilation » mais il ne s’agit pas de compilation dans le même sens du terme que pour JIT.

Le JIT consiste à « compiler » certaines portions de code en code natif. Ici pour Ruby on parle de MJIT pour « Method Based JIT » c’est-à-dire que les boucles « chaudes » ne seront pas candidates à la compilation mais que seules les fonctions sont concernées.

Les mesures de performances sont présentées avec « optcarrot » qui est un émulateur de console. C’est une bonne chose puisque c’est un cas d’usage réel (les calculs mathématiques répétitifs sont parfois utilisés pour les benchmarks de JIT, mais ils sont peu représentatifs car ils leur sont très favorables). D’un autre côté, les exécutions transactionnelles (typiquement des appels web à un site web en Ruby On Rails) seront beaucoup moins sensibles aux améliorations du JIT.

Le JIT n’est pas une nouveauté de Ruby 3.0.0, il a en fait été introduit dans Ruby 2.6.

Autre bonne nouvelle, en Ruby 3.0, la taille du JIT généré a fortement été réduite !

Détails de fonctionnement du MJIT

MJIT signifie que ce sont les fonctions qui sont candidates à la compilation. Il s’agit donc de remplacer des fonctions interprétées par des fonctions compilées sur la pile d’appel de fonctions.

Le MJIT de Ruby a une approche d’observation. Les méthodes ne sont pas compilées de manière trop optimiste (ce qui semble avoir été un reproche à la JVM par le passé, qui se traduisait par un ralentissement au démarrage) mais les appels de méthodes sont notés et après plusieurs appels, une fonction est mise en attente « pour compilation ». Les approches pessimistes gâchent des gains possibles, les approches optimistes pénalisent le démarrage quant aux approches « spéculatives », elles nécessitent un mécanisme de « dé-optimisation ».

Dans la pile d’attente on retrouve les fonctions à optimiser, attendant leur tour pour être compilées. L’exécution continue donc en parallèle et lorsque la fonction est compilée par ce processus externe, il peut remplacer la version interprétée sur la pile d’appel.

La compilation se fait en utilisant un compilateur disponible sur la plateforme. Tout simplement en générant un code source en C, puis en utilisant GCC pour compiler:
MJIT queue compilation

Image tirée de The method JIT compiler for Ruby 2.6 par k0kubun.

L’avantage de ce processus est sa simplicité et de pouvoir « s’acheter » facilement de la portabilité (JIT partout ou GCC est disponible) et également de bénéficier de l’optimisation de code des compilateurs.

Mais cela pose tout de même certains problèmes :

  • accès en écriture nécessaire pour Ruby sur une portion du système hôte ;
  • nécessité de disposer des outils de développement sur la machine hôte (vous avez GCC sur vos machines de production ? Pas moi !)

Ractor

Ruby 3.0.0 introduit également les Ractor. Il s’agit d’un modèle de concurrence basé sur les messages.

Les ractors permettent de gérer plus facilement les exécutions concurrentes tout en aidant à l’isolation des données. Les ractors sont une abstraction de concurrence et ils se veulent thread safe (partagent moins que des threads) sans pour autant le garantir. Chaque ractor possède un ou plusieurs fils d’exécution.

Les Ractor sont expérimentaux et n’acceptent pas toute la syntaxe Ruby.

Fibers/coroutines

Ruby 3.0 introduit également de nouvelles méthodes pour la concurrence légère avec des nouvelles routines de contrôle.

Notre époque sera statique ou ne sera pas

Depuis de nombreuses années, les langages interprétés à typage dynamique proposent des annotations pour donner les types de manière « statique ».

RBS

Ruby 3.0 étend sa syntaxe grâce à RBS qui est une gem qui apporte une grammaire de définitions.
RBS permet d’ajouter la syntaxe pour des types avancés comme les unions, la surcharge de méthodes, les génériques ou le duck typing avec interfaces.

Une définition RBS pourra ressembler à ceci :

# Classes
class User
  attr_reader name : String
  attr_reader age : Integer
  def initialize : (name: String, age: Integer) -> [String, Integer]
end

TypeProf

TypeProf est un analyseur de programme qui permet de générer (déduire) les annotations (la grammaire RBS vue précédemment) à partir d’un programme Ruby.
TypeProf n’est pas encore compatible avec toute la syntaxe Ruby.

À terme, on pourra utiliser le source Ruby avec ses annotations pour faire tout ce qu’on fait habituellement avec de l’analyse statique de code.

Autres nouveautés de Ruby 3.0.0

L’assignement avec la flèche => par exemple 42 => linuxfr.

Nouveau comportement de in:

  • in retourne à présent un booléen ;
  • pour trouver un pattern ;
  • case/in perd son statut « expérimental ».

Nouvelle syntaxe : des méthodes « sans fin » (sans mot clé end) comme par exemple

def square(x) = x * x

Conclusion

Ruby sort donc sa version majeure qui se concentre sur les performances, l’exécution concurrente et le typage statique.

Souhaitons bonne route à la branche 3 de Ruby tout en rappelant que le site sur lequel vous lisez cette dépêche est lui-même écrit en… Ruby !

Aller plus loin

  • # Les grands esprits se rencontrent.

    Posté par  . Évalué à 9 (+7/-0).

    performances, l’exécution concurrente et le typage statique.

    J'ai déjà entendu ça quelque part… Manque plus qu'un déploiement facilité par un binaire.

    Est-ce qu'on est entrain d'assister à une convergence des langages ou du moins des critères d'intérêt ?

  • # Typage fort

    Posté par  . Évalué à 10 (+15/-0).

    « Depuis de nombreuses années, les langages interprétés à typage faible… »

    Sauf erreur de ma part, Ruby comme Python sont à typage fort mais dynamique. Ce qui intrinsèquement est important.

    • [^] # Re: Typage fort

      Posté par  . Évalué à 3 (+4/-2). Dernière modification le 30/08/21 à 18:05.

      En fait, c'est surtout un langage purement objet (et non orienté objet), ce qui inclus que tout soit typé par défaut. En même temps la philosophie de Ruby d'être souple dans son utilisation des objets est que chaque type d'objet a tendance à être accessible, grâce aux grand nombre de méthodes de conversions de type (cast), comme d'autres type d'objets, de façon relativement transparente et généralisée.

      Donc je crois qu'on peut le considérer comme ayant un typage très fort (pur objet) et un typage très faible à la fois.

  • # endless methods

    Posté par  . Évalué à 2 (+1/-0).

    Peut-on avoir des exemples de méthodes sans fin (je trouve ça confus donc je demande une image plus claire que les exemples d'anciens codes que j'ai sous la main) et quel est l'intérêt/bénéfice ?

    • [^] # Re: endless methods

      Posté par  . Évalué à 3 (+1/-0).

      Dans l'annonce tu as ça :

      def square(x) = x * x
      • [^] # Re: endless methods

        Posté par  . Évalué à 3 (+2/-0).

        Dommage de ne pas avoir copié cette exemple dans l'annonce de LinuxFR.

        • [^] # Re: endless methods

          Posté par  . Évalué à 3 (+1/-0).

          Bonne idée, je l'ai rajouté.

          Merci.

          En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

          • [^] # Re: endless methods

            Posté par  . Évalué à 10 (+13/-0).

            Quitte à je ne pense pas que l'on puisse traduire le endless par "sans fin". Il n'est pas question de ne pas avoir de fin, mais de ne pas avoir de mot clef end et parler de "fonctions sans end" me semble toute de suite plus clair (là où au départ je pensais qu'il était question de fonctions qui ne retournent jamais1).

            • [^] # Re: endless methods

              Posté par  . Évalué à 3 (+2/-0).

              Mea culpa. Mais mon titre en anglais était bien lié à ma confusion qui va dans le sens… (j'avais bien compris « sans le mot clef "end" » mais me demandait justement comment était détecté la fin ou s'il s'agissait de ne plus du tout avoir de fin… en fait il s'agit juste de permettre l'écriture uniligne de fonctions triviales si j'ai bien tout compris)

          • [^] # Re: endless methods

            Posté par  . Évalué à 2 (+1/-0).

            Merci tout le monde.

  • # sur le typage

    Posté par  (site Web personnel) . Évalué à 7 (+6/-0). Dernière modification le 31/08/21 à 10:21.

    C'est bien d'avoir du typage explicite (on peut notamment imaginer d'utiliser ça dans un language server/IDE pour avoir de meilleures suggestions, et ça peut permettre de faire des vérifications à la compilation JIT ou via un outil dédié j'imagine? et donc d'avoir de meilleures garanties sur ce que fait le programme).

    Dommage d'avoir les déclarations de type dans un fichier séparé, je trouve ça particuliérement pas pratique.
    Ça aurait été mieux de l'avoir directement dans les méthodes/classes.
    Un peu l'impression que sur ça (notamment) ruby a un train de retard.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n’en sommes pas responsables.