Artikelserie
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.
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.
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.
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.
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.
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.
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.
Im Stammverzeichnis des Projekts
cargo build --release
Wenn Sie dies tun, wird es kompiliert.
build
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.
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!
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!
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).
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.
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