Ruby / Rust-Kooperation (3) Numerische Berechnung mit FFI

Artikelserie

Einführung

Versuchen wir, eine Rust-Funktion aufzurufen, die einfache numerische Berechnungen von Ruby mithilfe von FFI (Foreign Function Interface) ausführt. Es gibt bereits mehrere Artikel mit einem solchen Thema, es ist also das N-te Gebräu.

Darüber hinaus sollte beachtet werden.

Es wurde benutzt.

Gegenstand

Wie auch immer, ich möchte etwas praktischer machen als die Anzahl der Fibonacci. Okay, lass uns das machen. Es ist eine 3-Variablen-Version von Math. # Hypot. Es kann als dreidimensionale Version bezeichnet werden.

Die seltsam benannte Modulfunktion Math.hypot ist grundsätzlich

\sqrt{ x^2 + y^2 }

Etwas, das berechnet. Es hat diesen Namen, weil es die Länge der Hypotenuse aus den Längen zweier orthogonaler Seiten eines rechtwinkligen Dreiecks berechnet, $ x $ und $ y $.

p Math.hypot(3, 4) # => 5.0

Die Drei-Variablen-Version davon ist kurz gesagt

\sqrt{ x^2 + y^2 + z^2 }

Es ist eine Funktion, die berechnet.

Motivation

Sprechen Sie darüber, warum Sie eine 3-Variablen-Version von "Math.hypot" möchten. Es hat nichts mit dem Thema des Artikels zu tun, sodass Sie mit dem nächsten Abschnitt fortfahren können.

Diese Funktion kann verwendet werden, um den Abstand zwischen zwei Punkten bei zwei Koordinaten in der Ebene zu ermitteln. Mit anderen Worten

p1 = [1, 3]
p2 = [2, -4]

distance = Math.hypot(p1[0] - p2[0], p1[1] - p2[1])

Und so weiter. Wenn Sie [Vector] verwenden (https://docs.ruby-lang.org/ja/2.7.0/class/Vector.html)

require "matrix"

p1 = Vector[1, 3]
p2 = Vector[2, -4]

distance = (p1 - p2).norm

Ich kann aber gehen.

Die Geschichte ging schief.

Wenn es dann um den Abstand zwischen zwei Punkten in einem dreidimensionalen Raum geht, wollen wir natürlich eine dreidimensionale Version von "Hypot". Nein, natürlich macht es die Verwendung von "Vektor" einfach, eine beliebige Anzahl von Dimensionen wie oben beschrieben zu schreiben, aber es kann vorkommen, dass Sie "Vektor" aus Geschwindigkeits- oder anderen Gründen nicht verwenden möchten.

3 Variable Version

def Math.hypot3(x, y, z)
  Math.sqrt(x * x + y * y + z * z)
end

Kann leicht definiert werden. Der Grund für die Verwendung von "x * x" anstelle von "x ** 2" ist übrigens, dass das erstere sehr langsam ist [^ square].

[^ square]: Mit der jüngsten Quadratoptimierung ist es wahrscheinlich, dass x ** 2 um Ruby 3.0 nicht zu langsam ist.

Die obige Methode hat jedoch 3 Multiplikationen, 2 Additionen und 1 "sqrt", und da dies alles Methodenaufrufe sind, werden die Methoden insgesamt 6 Mal aufgerufen. Solche Ausdrücke können schneller sein, wenn sie in Rust oder C umgeschrieben werden.

Implementierung: Rostseite

Ich werde es schreiben, damit auch Leute, die mit Rust nicht vertraut sind, es reproduzieren können. Es wird jedoch davon ausgegangen, dass Rust installiert wurde.

Projekterstellung

Zuerst am Terminal

cargo new my_ffi_math --lib

Und mache ein Rust-Projekt. my_ffi_math ist der Projektname. Die Option "--lib" gibt an, dass "eine Bibliothekskiste erstellen" soll. Eine Kiste ist eine Zusammenstellungseinheit,

Es gibt zwei Arten.

Cargo.toml bearbeiten

Im Stammverzeichnis des Projekts befindet sich eine Datei namens Cargo.toml. Hier werden verschiedene Einstellungen für das gesamte Projekt geschrieben. Am Ende dieser Datei

Cargo.toml


[lib]
crate-type = ["cdylib"]

Ich werde hinzufügen. Die Bedeutung davon ist vom Autor nicht gut verstanden.

Eine Funktion erstellen

Es sollte eine Datei namens src / lib.rs. Die Testcodevorlage ist hier geschrieben [^ test], aber Sie können sie löschen.

[^ test]: Mit Rust können Sie Testcode in den Code schreiben, und das Ausführen des Tests ist sehr einfach. Führen Sie einfach einen "Frachttest" durch.

Und

src/lib.rs


#[no_mangle]
pub extern fn hypot3(x: f64, y: f64, z: f64) -> f64 {
    (x * x + y * y + z * z).sqrt()
}

Schreiben.

Ein Schlüsselwort, bei dem fn die Definition einer Funktion darstellt. pub und extern sind Boni dafür, nun, ich kann sie nicht richtig erklären. Ich denke, "Pub" ist so etwas wie "Ich werde diese Funktion der Außenwelt aussetzen".

f64 stellt einen Typ dar, der als 64-Bit-Gleitkommazahl bezeichnet wird und Ruby's Float through FFI entspricht. -> repräsentiert den Rückgabetyp der Funktion. Der Inhalt der Funktion kann durch Betrachten verstanden werden. Vor der Funktionsdefinition

#[no_mangle]

Ich bin neugierig. Ich bin mir nicht sicher, aber wenn ich das nicht schreibe, scheint es, dass ich, obwohl ich die Funktion mit dem Namen "hypot3" definiert habe, nach der Kompilierung nicht mit diesem Namen darauf verweisen kann.

Das ist alles für die Implementierung auf der Rust-Seite.

kompilieren

Im Stammverzeichnis des Projekts

cargo build --release

Wenn Sie dies tun, wird es kompiliert. build build </ ruby> ist eine coole Art zu kompilieren (? Nein, vielleicht nicht) Es gibt zwei Möglichkeiten zum Erstellen, eine zum Debuggen und eine zum Freigeben (Produktion), und "--release" bedeutet wörtlich "Erstellen für die Veröffentlichung". Die Ausführungsgeschwindigkeit für das Debuggen ist langsam.

Die kompilierte Version sollte sich im Pfad target / release / libmy_ffi_math.dylib befinden. Oh nein, die Erweiterung des Dateinamens ist Iloilo. Als ich es unter macOS gemacht habe, wurde es zu ".dylib", aber unter Windows sollte es anders sein [^ libext].

[^ libext]: Um genau zu sein, denke ich, dass die Erweiterung des Produkts nicht davon abhängt, auf welchem Betriebssystem es kompiliert wurde, sondern auf welchem Ziel es kompiliert wurde. Mit anderen Worten, wenn Sie es für macOS (x86_64-apple-darwin) unter Windows kompilieren, ist es ".dylib". Rost ist leicht zu kompilieren.

Dies ist die einzige Datei, die Ruby benötigt.

In diesem Fall ist der Basisname des Dateinamens (der Teil ohne die Erweiterung) der Projektname mit "lib" am Anfang. Wenn Sie name zu [lib] in Cargo.toml hinzufügen

Cargo.toml


[lib]
name = "hoge"
crate-type = ["cdylib"]

Der Dateiname sollte wie folgt aussehen: libhoge.dylib.

Implementierung: Ruby Seite

Verwenden Sie auf der Ruby-Seite das Juwel ffi (Sie können auch fiddle verwenden).

Einfach mit dem Code unten

require "ffi"

Aber natürlich, wenn Sie es mit Gemfile schaffen

Gemfile


gem "ffi", "~> 1.13"

Schreiben Sie es in ein Skript

require "bundler"
Bundler.require

Und so weiter.

Nehmen Sie außerdem an, dass sich der Ruby-Code vorerst im Stammverzeichnis des Rust-Projekts befindet.

Schreiben Sie so.

require "ffi"

module FFIMath
  extend FFI::Library
  ffi_lib "target/release/libmy_ffi_math.dylib"
  attach_function :hypot3, [:double, :double, :double], :double
end

p FFIMath.hypot3(1, 2, 3)
# => 3.7416573867739413

#Referenz
p Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)
# => 3.7416573867739413

Das Ausführungsergebnis wird ebenfalls in den Kommentar geschrieben, es ist jedoch ersichtlich, dass das Ergebnis mit der Berechnung in Ruby übereinstimmt.

Es spielt keine Rolle, was Sie als "FFIMath" eingestellt haben, also habe ich gerade ein schönes Modul vorbereitet. FFI :: Library`` expand für dieses Modul. Dies führt zu einigen singulären Methoden wie ffi_lib.

Die Methode ffi_lib gibt den Pfad der kompilierten Bibliotheksdatei an. Nun, ich bin mir nicht sicher, aber es scheint, dass relative Pfade möglicherweise nicht funktionieren, daher scheint es besser, einen absoluten Pfad anzugeben. Verwenden Sie dazu File.expand_path.

  ffi_lib File.expand_path("target/release/libmy_ffi_math.dylib", __dir__)

Und. Wenn Sie so schreiben, ist der relative Pfad vom Speicherort dieser Datei (__dir__) ein absoluter Pfad.

attach_function erstellt eine von Rust erstellte Funktion als singuläre Methode des Moduls. Das erste Argument ist der Funktionsname. Das zweite Argument gibt den Typ des Funktionsarguments an. Da es 3 Argumente hat, ist es ein Array der Länge 3. : double repräsentiert eine Gleitkommazahl mit doppelter Genauigkeit in FFI. Es entspricht f64 in Rust und Float in Ruby. Das dritte Argument gibt den Rückgabetyp der Funktion an. Mit dem Obigen kann die singuläre Methode des Moduls definiert werden.

Wie Sie sehen können, wie man es benutzt. Wenn Sie eine gute Idee haben, fragen Sie sich vielleicht: "Hmm? Sie müssen dem Argument Float geben, aber Sie geben Integer?" Ich bin damit nicht vertraut, aber ich bin sicher, dass ffi gem es in eine Gleitkommazahl umgewandelt hat.

Es war überraschend einfach!

Prüfstandstest

Ich hatte auf diese Geschwindigkeit gehofft, als ich versuchte, hypot3 in Rust zu implementieren. Dann müssen Sie es durch einen Benchmark-Test beweisen. Nun, wie schnell wird es sein!

Testbibliothek

Bei der Messung der Lichtverarbeitung wie "hypot3" denke ich, dass die Benchmark-Testbibliothek [Benchmark_driver] ist (https://github.com/benchmark-driver/benchmark-driver).

Um die Lichtverarbeitung zu messen, muss die Zeit gemessen werden, zu der dieselbe Verarbeitung viele Male ausgeführt wird. Wenn Sie jedoch eine Schleife mit der Methode "times" usw. ausführen, sind die Kosten der Schleife relativ vernachlässigbar und können nicht genau gemessen werden. .. Es scheint, dass Benchmark_Driver die tatsächliche Ausführungsgeschwindigkeit messen kann, da es viele Male ausgeführt wird, ohne dass solche Kosten entstehen (ich weiß nicht, wie es funktioniert).

Testcode

Verwendung des Benchmark-Treibers

Es gibt zwei Möglichkeiten, aber dieses Mal werde ich Letzteres versuchen. Letzteres erfordert, dass Sie sich an das YAML-Format erinnern, aber es ist nicht so schwierig.

Schreiben Sie wie folgt.

benchmark.yaml


prelude: |
  require "ffi"

  def Math.hypot3(x, y, z)
    Math.sqrt(x * x + y * y + z * z)
  end

  module FFIMath
    extend FFI::Library
    ffi_lib "target/release/libmy_ffi_math.dylib"
    attach_function :hypot3, [:double, :double, :double], :double
  end

  x, y, z = 3, 2, 7

benchmark:
  - Math.hypot3(x, y, z)
  - FFIMath.hypot3(x, y, z)

Schreiben Sie für den Inhalt von "Vorspiel" etwas, das vor der Messung durchgeführt werden sollte. Ich habe Math.hypot3 zum Vergleich definiert.

Testlauf

Wenn Sie dies tun können, am Terminal

benchmark-driver benchmark.yaml

Und. (Es ist verwirrend, dass der Befehlsname ein Bindestrich ist, obwohl der Edelsteinname unterstrichen ist.) Oh, installiere Benchmark_Driver Gem

gem i benchmark_driver

Lass es uns im Voraus tun.

Ergebnis ist ···

Comparison:
   Math.hypot3(x, y, z):  10211285.3 i/s
FFIMath.hypot3(x, y, z):   5153872.8 i/s - 1.98x  slower

wie?

"Math.hypot3" kann 10 Millionen Mal pro Sekunde ausgeführt werden, während "FFI Math.hypot3" 5 Millionen Mal pro Sekunde ausgeführt werden kann. Ist es nicht eine schreckliche Niederlage? Weit davon entfernt, schnell zu sein, ist es zu langsam, um über [^ osoi] zu sprechen.

[^ osoi]: Übrigens werden in diesem Code ganzzahlige Objekte an "x", "y", "z" vergeben, aber wenn ein Float-Objekt angegeben wird, verlangsamt sich "Math.hypot3" um mehr als 10%. Das FFIMath.hypot3 war unverändert.

Was ist die Ursache für die Niederlage? Rusts Code scheint sich nicht weiter zu verbessern. Für die Kompilierung habe ich den Release-Build richtig angegeben. Wenn man sich die verschiedenen Benchmarks von Rust ansieht, scheint es, dass es C nicht unterlegen ist, also scheint es, dass Rust nicht langsam ist.

Wenn es darum geht, sind es nicht die Kosten für FFI? Da Ruby alles als Argument übergeben kann, vermute ich, dass ffi gem den Typ zur Laufzeit überprüft und konvertiert. Eine solche zusätzliche (?) Verarbeitung kann eine Belastung sein.

Es stellte sich heraus, dass es keinen Sinn zu machen scheint, die Verarbeitung von etwa "hypot3" in Rust zu implementieren. Wir müssen schwerer verarbeiten.

Lassen Sie uns einen zweiten Blick darauf werfen und über etwas "Schwerere Verarbeitung" nachdenken.

Recommended Posts

Ruby / Rust-Kooperation (3) Numerische Berechnung mit FFI
Ruby / Rust-Verknüpfung (4) Numerische Berechnung mit Rutie
Ruby / Rust-Kooperation (5) Numerische Berechnung mit Rutie ② Veggie
Rubin / Rost-Kooperation (6) Extraktion der Morphologie
Objektorientierte numerische Berechnung
Bedingte numerische Berechnung
Rubin / Rost-Zusammenarbeit (1) Zweck
Ruby Score Berechnungsprogramm
Erste Schritte mit Ruby
Ausgabe der Rubinkalorienberechnung
Rubin / Rost-Kooperation (2) Mittel
Evolve Eve mit Ruby