2 points par GN⁺ 2025-02-14 | 1 commentaires | Partager sur WhatsApp

Y a-t-il un moyen d’améliorer les performances de la FFI de CRuby ?

  • Lorsqu’il faut appeler du code natif depuis Ruby, il vaut mieux écrire autant de code Ruby que possible. YJIT peut optimiser le code Ruby, mais pas le code C.
  • Lorsqu’on appelle une bibliothèque native, il est préférable d’effectuer l’essentiel du travail en Ruby et d’écrire une extension native fournissant une API simple pour invoquer les fonctions natives.
  • La FFI n’offre pas les mêmes performances qu’une extension native. Par exemple, si l’on encapsule la fonction C strlen via la FFI, les performances sont inférieures à celles d’une extension C.

Résultats du benchmark

  • Appeler directement String#bytesize est le plus rapide, et peut servir de référence.
  • L’appel à strlen via une extension C est le deuxième plus rapide, suivi par l’appel indirect à String#bytesize.
  • L’implémentation FFI est la plus lente. Cela montre qu’appeler une fonction native via la FFI entraîne un surcoût important.

Peut-on changer la donne ?

  • À partir d’une idée de Chris Seaton, l’auteur explore la possibilité de générer du code JIT pour appeler des fonctions externes.
  • Dans l’exemple d’un wrapper FFI, il est possible de générer le code machine nécessaire au moment où la fonction wrapper est définie, lors de l’appel à attach_function.

Utilisation de RJIT

  • RJIT est un compilateur JIT écrit en Ruby et fourni avec Ruby.
  • RJIT est extrait sous forme de gem afin de permettre à des compilateurs JIT tiers de faire plus facilement le mapping des structures de données Ruby.
  • Le pointeur de fonction d’entrée du JIT est toujours exécuté afin qu’un JIT tiers puisse s’enregistrer avec du code machine.

Preuve de concept

  • Une petite preuve de concept appelée « FJIT » permet d’appeler des fonctions externes en générant du code machine à l’exécution.
  • D’après les benchmarks, le code machine généré par FJIT est plus rapide qu’une extension C et plus de deux fois plus rapide qu’un appel FFI.

Conclusion

  • Cela montre qu’il est possible d’écrire autant de code Ruby que possible tout en conservant la même vitesse qu’une extension C, voire une vitesse supérieure.
  • Ruby pourrait ainsi bénéficier de la capacité à appeler du code natif sans FFI.

Points à noter

  • Pour l’instant, cela est limité à la plateforme ARM64. Il faut ajouter un backend x86_64.
  • Tous les types de paramètres et de retour ne sont pas pris en charge. Seul un paramètre unique et une valeur de retour unique peuvent être gérés.
  • Il faut exécuter Ruby avec les flags --rjit --rjit-disable. Cela devrait être résolu lorsque la fonctionnalité de Kokubun sera appliquée.
  • Cela ne fonctionne actuellement que sur la version head de Ruby.

1 commentaires

 
GN⁺ 2025-02-14
Commentaires sur Hacker News
  • Il a fallu beaucoup travailler sur la FFI pour les appels de fonctions entre le solveur de contraintes Java (Timefold) et CPython

    • Les problèmes de performances de la FFI viennent principalement de l’utilisation de proxys pour la communication entre le langage hôte et le langage étranger
    • Les appels FFI directs via JNI ou la nouvelle interface foreign sont rapides, à une vitesse proche d’un appel direct de méthode Java
    • Cependant, les garbage collectors de CPython et de Java ne s’accordent pas bien, donc des techniques particulières sont nécessaires pour la synchronisation
    • L’utilisation de proxys comme JPype ou GraalPy entraîne un surcoût de performance, car il faut convertir les paramètres et les valeurs de retour, et cela peut provoquer des appels FFI supplémentaires
    • Lorsqu’un objet CPython est transmis à Java, Java détient un proxy de cet objet CPython
    • Si ce proxy est ensuite retransmis à CPython, un proxy de proxy est créé
    • Au final, les proxys JPype sont 1402 % plus lents qu’un appel direct à CPython via FFI, et les proxys GraalPy sont 453 % plus lents
    • Finalement, le bytecode CPython est converti en bytecode Java, et des structures de données Java correspondant aux classes CPython utilisées sont générées
    • Résultat : un gain de performance 100 fois supérieur à l’utilisation de proxys
    • Convertir ou lire le bytecode CPython est très instable et mal documenté, et il est difficile de le mapper directement vers un autre bytecode à cause de diverses particularités de la VM
    • Pour plus de détails, voir le billet de blog : lien
  • Grâce au blog de Rails At Scale et à celui de byroot, c’est actuellement un très bon moment pour s’intéresser aux discussions approfondies sur les internals et les performances de Ruby

    • Avec les améliorations récentes de Ruby et Rails, c’est une bonne période pour être Rubyist
  • Question sur la possibilité de compiler le code en JIT pour les appels de fonctions externes au lieu d’appeler une bibliothèque tierce

    • Presque certain que c’est le principe de base de la FFI de LuaJIT : lien
    • C’est probablement la raison pour laquelle la FFI de LuaJIT est si rapide
  • Informations sur une bibliothèque qui utilise JVMCI pour générer à la volée du code arm64/amd64 afin d’appeler des bibliothèques natives sans JNI : lien

  • Avis du type : « écrivez autant de Ruby que possible, notamment parce que YJIT peut optimiser le code Ruby, mais pas le code C »

    • Question sur le fait que Ruby n’est-il pas un langage plutôt lent
    • Si l’on passe en natif, on voudrait faire autant de travail que possible côté natif
  • J’utilise Ruby depuis plus de 10 ans, et voir les progrès récents est vraiment très intéressant

    • Enthousiaste pour la suite
  • Interrogation sur la raison d’avoir besoin de compilation JIT

    • Si on peut l’écrire en C, ne pourrait-on pas le compiler au chargement ?
  • FFI - Foreign Function Interface, autrement dit la manière d’appeler du C depuis Ruby

  • Question de savoir si ce n’est pas exactement ce que fait libffi

  • Je crois comprendre pourquoi ils ne sont pas allés sur tenderlovemaking.com