2 Punkte von GN⁺ 2025-02-14 | 1 Kommentare | Auf WhatsApp teilen

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

 
GN⁺ 2025-02-14
Hacker-News-Kommentare
  • Musste viel mit FFI arbeiten, um Funktionsaufrufe zwischen dem Java Constraint Solver (Timefold) und CPython zu ermöglichen

    • Das Performance-Problem von FFI entsteht hauptsächlich durch die Verwendung von Proxys für die Kommunikation zwischen Host-Sprache und Fremdsprache
    • Direkte FFI-Aufrufe mit JNI oder der neuen Foreign Function Interface sind schnell und etwa so flott wie ein direkter Aufruf einer Java-Methode
    • Allerdings passen die Garbage Collector von CPython und Java nicht gut zusammen, daher sind spezielle Techniken zur Synchronisierung nötig
    • Verwendet man Proxys wie JPype oder GraalPy, entsteht Performance-Overhead; Parameter und Rückgabewerte müssen konvertiert werden, und es können zusätzliche FFI-Aufrufe anfallen
    • Wenn ein CPython-Objekt an Java übergeben wird, hat Java einen Proxy für das CPython-Objekt
    • Wenn dieser Proxy wieder an CPython zurückgegeben wird, entsteht ein Proxy eines Proxys
    • Dadurch sind JPype-Proxys 1402 % langsamer als ein direkter FFI-Aufruf von CPython, GraalPy-Proxys 453 % langsamer
    • Am Ende wurden CPython-Bytecode in Java-Bytecode umgewandelt und Java-Datenstrukturen erzeugt, die den verwendeten CPython-Klassen entsprechen
    • Das Ergebnis war ein 100-facher Performance-Gewinn gegenüber der Verwendung von Proxys
    • CPython-Bytecode zu transformieren oder zu lesen ist sehr instabil und schlecht dokumentiert, und wegen vieler Eigenheiten der VM lässt er sich nur schwer direkt auf anderen Bytecode abbilden
    • Weitere Details im Blogbeitrag: Link
  • Dank Rails At Scale und byroots Blog ist gerade ein guter Zeitpunkt, sich für tiefgehende Diskussionen über Ruby-Interna und Performance zu interessieren

    • Dank der jüngsten Verbesserungen bei Ruby und Rails ist es derzeit eine gute Zeit, Rubyist zu sein
  • Frage, ob sich Code JIT-kompilieren lässt, statt für externe Funktionsaufrufe eine 3rd-party-Bibliothek aufzurufen

    • Bin ziemlich sicher, dass das das Grundprinzip von LuaJIT FFI ist: Link
    • Vermutlich ist das der Grund, warum das FFI von LuaJIT so schnell ist
  • 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“

    • Frage, ob Ruby nicht eine ziemlich langsame Sprache ist
    • Wenn man schon in Native geht, möchte man möglichst viel Arbeit auch dort erledigen
  • Nutze Ruby seit mehr als 10 Jahren, und es ist sehr spannend, die jüngsten Entwicklungen zu sehen

    • Freue mich darauf
  • Frage, warum dafür JIT-Kompilierung nötig ist

    • Wenn man es in C schreiben kann, müsste man es dann nicht beim Laden kompilieren können?
  • 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