Gibt es eine Möglichkeit, die FFI-Geschwindigkeit von CRuby zu verbessern?
- Wenn in Ruby nativer Code aufgerufen werden muss, ist es am besten, möglichst viel Ruby-Code zu schreiben. Der Grund ist, dass YJIT Ruby-Code optimieren kann, C-Code jedoch nicht.
- Beim Aufruf nativer Bibliotheken ist es sinnvoll, den Großteil der Arbeit in Ruby zu erledigen und eine native Erweiterung zu schreiben, die eine einfache API für den Aufruf nativer Funktionen bereitstellt.
- FFI bietet nicht die gleiche Leistung wie native Erweiterungen. Wenn man zum Beispiel die C-Funktion
strlen über FFI kapselt, ist die Performance im Vergleich zu einer C-Erweiterung schlechter.
Benchmark-Ergebnisse
- Der direkte Aufruf von
String#bytesize ist am schnellsten und kann als Referenzwert betrachtet werden.
- Der
strlen-Aufruf über eine C-Erweiterung ist am zweitschnellsten, danach folgt der indirekte Aufruf von String#bytesize.
- Die FFI-Implementierung ist am langsamsten. Das zeigt, dass beim Aufruf nativer Funktionen über FFI erheblicher Overhead entsteht.
Lässt sich die Realität verändern?
- Auf Grundlage einer Idee von Chris Seaton wird derzeit die Möglichkeit untersucht, JIT-Code zu erzeugen, um externe Funktionen aufzurufen.
- Im Beispiel eines FFI-Wrappers könnte beim Aufruf von
attach_function der benötigte Maschinencode bereits beim Definieren der Wrapper-Funktion erzeugt werden.
Nutzung von RJIT
- RJIT ist ein in Ruby geschriebener JIT-Compiler, der zusammen mit Ruby ausgeliefert wird.
- RJIT wird als gem ausgelagert, damit 3rd-Party-JIT-Compiler Ruby-Datenstrukturen einfacher abbilden können.
- Es wird immer ein JIT-Entry-Function-Pointer ausgeführt, damit sich 3rd-Party-JITs in den Maschinencode einklinken können.
Proof of Concept
- Mit einem kleinen Proof of Concept namens "FJIT" lässt sich zur Laufzeit Maschinencode erzeugen, um externe Funktionen aufzurufen.
- Die Benchmark-Ergebnisse zeigen, dass der von FJIT erzeugte Maschinencode schneller als eine C-Erweiterung und mehr als doppelt so schnell wie ein FFI-Aufruf ist.
Fazit
- Das zeigt das Potenzial, möglichst viel Ruby-Code zu schreiben und dabei die gleiche Geschwindigkeit wie C-Erweiterungen beizubehalten oder sogar zu übertreffen.
- Ruby könnte damit den Vorteil erhalten, nativen Code ohne FFI aufrufen zu können.
Hinweise
- Der Ansatz ist derzeit auf die ARM64-Plattform beschränkt. Ein x86_64-Backend muss noch hinzugefügt werden.
- Nicht alle Parametertypen und Rückgabetypen werden unterstützt. Derzeit sind nur ein einzelner Parameter und ein einzelner Rückgabewert möglich.
- Ruby muss mit den Flags
--rjit --rjit-disable ausgeführt werden. Das dürfte gelöst sein, sobald die Funktion von Kokubun übernommen wurde.
- Derzeit läuft es nur auf Ruby-Head.
1 Kommentare
Hacker-News-Kommentare
Musste viel mit FFI arbeiten, um Funktionsaufrufe zwischen dem Java Constraint Solver (Timefold) und CPython zu ermöglichen
Dank Rails At Scale und byroots Blog ist gerade ein guter Zeitpunkt, sich für tiefgehende Diskussionen über Ruby-Interna und Performance zu interessieren
Frage, ob sich Code JIT-kompilieren lässt, statt für externe Funktionsaufrufe eine 3rd-party-Bibliothek aufzurufen
Hinweis auf eine Bibliothek, die mit JVMCI on the fly arm64-/amd64-Code erzeugt und damit ohne JNI native Bibliotheken aufrufen kann: Link
Meinung: „Schreibe so viel Ruby wie möglich, besonders weil YJIT Ruby-Code optimieren kann, C-Code aber nicht“
Nutze Ruby seit mehr als 10 Jahren, und es ist sehr spannend, die jüngsten Entwicklungen zu sehen
Frage, warum dafür JIT-Kompilierung nötig ist
FFI – Foreign Function Interface, also die Möglichkeit, aus Ruby heraus C aufzurufen
Frage, ob das nicht genau das ist, was libffi macht
Ich glaube, ich weiß, warum man nicht auf tenderlovemaking.com gegangen ist