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
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
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
Question sur la possibilité de compiler le code en JIT pour les appels de fonctions externes au lieu d’appeler une bibliothèque tierce
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 »
J’utilise Ruby depuis plus de 10 ans, et voir les progrès récents est vraiment très intéressant
Interrogation sur la raison d’avoir besoin de compilation JIT
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