Ruby / Rust-Verknüpfung (4) Numerische Berechnung mit Rutie

Einführung

Letztes Mal über FFI

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

Ich habe Rusts Funktion aufgerufen, um aus Ruby zu berechnen. Es war eine Ernte, die sich als überraschend einfach umzusetzen erwies, aber die Schlüsselgeschwindigkeit liegt in Ruby.

Math.sqrt(x * x + y * y + z * z)

Schade, dass es viel langsamer war als rechnen. Ich kenne die Ursache nicht, aber ich spekulierte, dass die Kosten für FFI hoch waren.

Warum also nicht versuchen, Rust und Ruby mit anderen Mitteln als FFI zu verbinden? Dieser Artikel verwendet etwas namens Rutie.

Qiita scheint keinen Artikel über Rutie zu haben, daher denke ich, dass dies der erste Artikel ist.

Darüber hinaus sollte beachtet werden.

Es wurde benutzt.

Was ist Rutie?

Offizielle Seite: danielpclark / rutie: „Die Verbindung zwischen Rubin und Rost.“

Rutie scheint wie "rooty" zu lesen.

Während FFI ein Allzweckmechanismus ist, der mehrere Sprachen verbindet, befindet sich Rutie nur zwischen Ruby und Rust. Eine große Funktion ist, dass Sie Ruby-Klassen, -Module und -Methoden in Rust schreiben können. Rust hat auch Typen, die Ruby's String, Array und Hash entsprechen. Es scheint auch, dass Ruby-Methoden von der Rust-Seite aufgerufen werden können.

Gegenstand

Gleich wie bei FFI

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

Schreiben Sie eine zu berechnende Funktion in Rust und rufen Sie sie in Ruby auf.

Im Fall von FFI war es so, als würde man einer Ruby-Methode eine Rust-Funktion zuweisen, während es im Fall von Rutie so war, als würde man eine Ruby-Methode direkt in Rust schreiben.

Implementierung: Rostseite

Wie bei FFI werde ich 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_rutie_math --lib

Und mache ein Rust-Projekt.

my_rutie_math ist der Projektname. Es wird ein gleichnamiges Verzeichnis erstellt, in dem die ersten Dateien gespeichert werden.

--lib ist eine Spezifikation, die besagt, dass" ich eine Bibliothekskiste machen werde ".

Cargo.toml bearbeiten

Das Ende von Cargo.toml im Projektstamm endet mit "[Abhängigkeiten]", was wie folgt lautet.

Cargo.toml


[dependencies]
rutie = "0.7.0"

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

(Ergänzung 2020-10-01) Die neueste Version von Rutie vom 2020-10-01 ist 0.8.1. Wenn Sie es also von nun an ausprobieren möchten

rutie = "0.8.1"

Bitte. [dependencies]

Wo "[Abhängigkeiten]" ist, wird die Abhängigkeitskiste angegeben. In Ruby ist es so, als würde man einen abhängigen Edelstein in einer Gemfile angeben. Hier sagt er, dass er die Kiste [Rutie] verwenden wird (https://crates.io/crates/rutie). "0.7.0" ist die Versionsspezifikation der Rutie-Kiste, bedeutet jedoch nicht "make it version 0.7.0", sondern "Version 0.7.0 oder mehr und weniger als 0.8.0". Mit anderen Worten, es ist dasselbe wie die Angabe von "~> 0.7.0" in der Ruby Gemfile.

Ab dem 4. September 2020 war die neueste Version von Rutie Crate Version 0.8.0, aber aus irgendeinem Grund hat 0.8.0 nicht funktioniert [^ dame], daher werde ich die Ursachenuntersuchung verschieben und eine ältere Version 0.7 verwenden Fahren Sie mit .0 fort.

[^ dame]: Ich habe es mit macOS und msvc und gnu von Windows versucht, aber beim Kompilieren erhalte ich in der Verbindungsphase einen Fehler. Ich habe es nicht im Detail untersucht, aber wenn ich eine Chance habe, möchte ich es in einem anderen Artikel zusammenfassen.

Bitte beachten Sie, dass die Erklärung auf der offiziellen Website unter der Prämisse 0.8.0 verfasst ist.

(Ergänzung 2020-10-01) Danach, als ich 0.8.0 unter macOS erneut versuchte, gab es kein besonderes Problem. 0.8.1 war auch okay. Vielleicht hat sich etwas geändert. Bitte lassen Sie mich wissen, wenn Sie einen Fehler im Build erhalten.

[lib]

Ich bin mir nicht sicher, was das nächste "[lib]" bedeutet.

crate-type gibt buchstäblich den Kistentyp an. Von der Binärkiste und der Bibliothekskiste wird die Bibliothekskiste erstellt, aber es scheint tatsächlich verschiedene Arten von Bibliothekskisten zu geben. Die folgenden Artikel sind nützlich.

Ich habe versucht, Rusts crate_type --Qiita zusammenzufassen

cdylib scheint eine dynamische Bibliothek für andere Sprachen (dh andere Sprachen als Rust) zu bedeuten. Nun, bedeutet "dy" dynamisch und "c" bedeutet C-Sprache?

Modul- und Methodenbeschreibung

Dann die Beschreibung des Hauptkörpers.

Mit Rutie können Sie Ruby-Module und -Klassen erstellen. Dieses Mal möchte ich nur eine Funktion (Zielmethode) erstellen. Machen wir sie also zu einem Modul anstelle einer Klasse. Der Modulname sollte "MyMath" sein. Nennen Sie die Methode "hypot3" und definieren Sie sie als singuläre Methode von "MyMath". Die Richtlinie wurde festgelegt.

Machen Sie die Datei src / lib.rs wie folgt. Ursprünglich ist die Testcodevorlage geschrieben, aber Sie können sie löschen.

src/lib.rs


#[macro_use]
extern crate rutie;

use rutie::{Object, Module, Float};

module!(MyMath);

methods!(
    MyMath,
    _rtself,

    fn pub_hypot3(x: Float, y: Float, z: Float) -> Float {
        let x = x.unwrap().to_f64();
        let y = y.unwrap().to_f64();
        let z = z.unwrap().to_f64();
        Float::new((x * x + y * y + z * z).sqrt())
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_MyMath() {
    Module::new("MyMath").define(|module| {
        module.def_self("hypot3", pub_hypot3)
    });
}

Der Umfang der Beschreibung ist etwas größer als bei der FFI-Version.

Makro

Achten Sie zuerst auf "Modul!" Und "Methoden!". Dies sind Makros, die in der Rutie-Kiste definiert sind und Ruby-Module und -Methoden zu definieren scheinen.

Um diese Makros am Anfang zu verwenden

#[macro_use]
extern crate rutie;

(Ich weiß es nicht).

Object, Module, Float

In Rutie scheinen die Rusttypen, die Klassen wie Object, Module, Class, Array, Float, Hash und Symbol of Ruby entsprechen, mit ** demselben Namen ** definiert zu sein. Die Ruby-Zeichenfolge heißt jedoch RString anstelle des gleichen Namens. Ich denke, ich habe R hinzugefügt, damit es sich nicht mit Rusts String überschneidet.

Dieses Mal benötigen wir drei davon: Object, Module und Float.

use rutie::{Object, Module, Float};

Ich schreibe das

Moduldefinition

So erstellen Sie ein Ruby-Modul namens "MyMath"

module!(MyMath);

Schreiben. Wahrscheinlich wird das eigentliche Modul hier nicht erstellt, aber wenn Sie Module :: new (" MyMath ") ausführen, was später erscheint.

Methodendefinition

Die Methodendefinition gibt dem Methoden! Makro drei Argumente. Das erste Argument ist der Modulname "MyMath". Ich habe keine Ahnung von dem zweiten Argument, "_rtself".

Die Definition der Funktion ist im dritten Argument angegeben. Lassen Sie uns extrahieren:

fn pub_hypot3(x: Float, y: Float, z: Float) -> Float {
    let x = x.unwrap().to_f64();
    let y = y.unwrap().to_f64();
    let z = z.unwrap().to_f64();
    Float::new((x * x + y * y + z * z).sqrt())
}

Zunächst wird dem Funktionsnamen "pub_" vorangestellt, ähnlich dem Codebeispiel auf der Rutie-Site, in dem angegeben wird, dass "dem Funktionsnamen" pub_ "vorangestellt ist, damit er sich nicht mit anderen überschneidet." Es ist nicht erforderlich, es anzubringen, wenn klar ist, dass es nicht getragen wird. Bitte seien Sie versichert, dass pub_hypot3 der Methodenname hypot3 auf der Ruby-Seite ist.

Übrigens sind sowohl das Argument als auch der Rückgabewert vom Typ "Float" anstelle von "f64". Float scheint hier definiert zu sein: https://github.com/danielpclark/rutie/blob/v0.7.0/src/class/float.rs

Die hier geschriebenen Kommentare finden Sie als Dokumentation unten: https://docs.rs/rutie/0.7.0/rutie/struct.Float.html

Hier stellte sich eine große Frage. Wo kann man von einem Argument zu "f64" konvertieren?

x.unwrap().to_f64()

Ich versuche. Gemäß dem vorherigen Dokument sollte "Float" mit "to_f64 ()" in "f64" konvertiert werden können. Warum beißen Sie unwrap ()? Hallo, der Typ dieses x ist

std::result::Result<rutie::Float, rutie::AnyException>

Scheint zu sein. Wahrscheinlich ist es "Ergebnis", denn wenn Sie den Wert von Ruby erhalten, wird Ihnen möglicherweise etwas Seltsames übergeben. Aus diesem Grund ruft unwrap () einen Wert vom Typ Float ab. Aber! Die Art der Funktion ist

fn pub_hypot3(x: Float, y: Float, z: Float) -> Float

War es nicht Es ist "Float", "x" ist. Ah ~?

Ich konnte es nicht durch meine Fähigkeit herausfinden, selbst wenn ich es nachschlug. Ich dachte jedoch, dass der Punkt sein könnte, dass dieser Teil ein Argument des Methoden! Makros ist. Ja, diese Funktionsdefinition ** - wie etwas ** wird an das Makro übergeben. Es ist nicht die Rust-Funktion selbst.

Lassen Sie uns dieses Problem beiseite legen und weitermachen. Im Funktionskörper

let x = x.unwrap().to_f64();

Es wird gesagt. Es ist das sogenannte Shadowing, das "x" mit demselben Namen definiert, obwohl das Argument "x" enthält. Das ursprüngliche "x" wird nicht mehr benötigt, verwenden Sie also eine Variable mit demselben Namen.

Letzte

Float::new((x * x + y * y + z * z).sqrt())

Generiert einen Float-Wert, der basierend auf dem berechneten f64-Wert an die Ruby-Seite zurückgegeben wird.

Definition der Initialisierungsfunktion

Der Begriff "Initialisierungsfunktion" ist etwas, das ich mir ausgedacht habe und das möglicherweise nicht angemessen ist. Definieren Sie auf jeden Fall eine Funktion, die von der Ruby-Seite aufgerufen werden soll. Auf diese Weise können die in Rust definierten Ruby-Module und -Methoden tatsächlich auf der Ruby-Seite verwendet werden.

Auszug unten.

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_mymath() {
    Module::new("MyMath").define(|module| {
        module.def_self("hypot3", pub_hypot3)
    });
}

Ich kenne die Namenskonvention des Funktionsnamens nicht, habe sie jedoch gemäß dem Codebeispiel in Form von "Init_XXXX" erstellt. Das Wort "# [allow (non_snake_case)]" am Anfang bedeutet wahrscheinlich, dass der Compiler mit "Beschweren Sie sich nicht, weil beabsichtigt war, es nicht zu einem Schlangenfall zu machen" feststeckt.

# [no_mangle] ist ein vertrauter Spruch, den die Bibliothek verwendet, wenn sie eine Funktion definiert, die nach außen angezeigt werden soll, und es scheint, dass der Funktionsname ohne ihn nicht mit diesem Namen bezeichnet werden kann.

Was den Inhalt der Funktion betrifft, so scheint es, dass das Modul "MyMath" zuerst mit "Module :: new (" MyMath ")" erstellt wird und die Methode mit "def_self" dafür erstellt wird. Das Argument von define ist

|module| {
    module.def_self("hypot3", pub_hypot3)
}

Es ist in Form von. Dies wird als Schließung bezeichnet. Es ist interessant, dass die Syntax Ruby-Blöcken sehr ähnlich ist. Der Unterschied besteht darin, dass der Teil, der dem Blockparameter von Ruby entspricht, außerhalb des {{} liegt. Ruby-Blöcke sind keine Werte (keine Objekte), aber Rust-Verschlüsse sind Werte und können an Funktionsargumente übergeben werden.

module.def_self (" hypot3 ", pub_hypot3) scheint zu bedeuten, dass das zuvor definierte pub_hypot3 im Modul MyMath als Methode mit dem Namen hypot3 erzeugt wird.

Damit ist die Implementierung auf der Rust-Seite in Ordnung. Es gab einige Dinge, die ich nicht verstand, und es fühlte sich ein bisschen verwirrend an. Aber wäre es nicht schön, wenn Ruby-Klassen, -Module und -Methoden mit dieser Komplexität definiert werden könnten?

kompilieren

Im Stammverzeichnis des Projekts

cargo build --release

Ich werde das machen. Das Artefakt befindet sich dann im Pfad "target / release / libmy_rutie_math.dylib". Die Erweiterung sollte jedoch je nach Ziel unterschiedlich sein. Ich frage mich, ob es unter Windows ".dll" sein wird.

(Zusatz) Beim Kompilieren

warning: `extern` fn uses type `MyMath`, which is not FFI-safe
 --> src/lib.rs:9:5
  |
9 |     MyMath,
  |     ^^^^^^ not FFI-safe

Wird angezeigt. (Ich habe die Ruby- und Rust-Versionen am Anfang des Artikels geschrieben.)

Der Name "MyMath" scheint "nicht FFI-sicher" zu sagen. Ich weiß nicht, was das bedeutet, aber es ist eine Warnung, kein Fehler, also werde ich es vorerst ignorieren.

(Ergänzung 2020-10-01) Die Warnung "nicht FFI-sicher" wurde in Rust 1.46 veröffentlicht. Es wurde in Rutie 0.8.1 gelöst. https://github.com/danielpclark/rutie/issues/128

Implementierung: Ruby Seite

Verwenden Sie auf der Rubinseite einen Edelstein namens "Rutie". Der gleiche Name wie die in Rust verwendete Kiste. Einfach zu verstehen.

Das folgende Beispielskript wird unter der Annahme geschrieben, dass es im Stammverzeichnis des Rust-Projekts vorhanden ist.

gem "rutie", "~> 0.0.4"
require "rutie"

Rutie.new(:my_rutie_math, lib_path: "target/release").init "Init_mymath", __dir__

p MyMath.hypot3(1.0, 2.0, 3.0)
# => 3.7416573867739413

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

Im obigen Beispiel habe ich versucht, es in einer Datei anzuzeigen, und plötzlich habe ich "gem" rutie "," ~> 0.0.4 "" gemacht, aber normalerweise habe ich es in Gemfile geschrieben.

Nun, wie man Rutie Gem benutzt, zunächst scheint es, ein Ruby-Objekt mit Rutie.new zu erstellen. Ich habe im ersten Argument : my_rutie_math geschrieben, das ist der Name der von Rust erstellten Bibliothek.

In diesem Artikel wird der Projektname, der beim ersten "Laden neuer" angegeben wurde, als Bibliotheksname verwendet. Aber bei [lib] in Cargo.toml

Cargo.toml


[lib]
name = "hoge"

Wenn Sie so einen Namen angeben, sollte dies der Name der Bibliothek sein. Und das sollte sich im Dateinamen des resultierenden Artefakts widerspiegeln.

Das optionale Argument "lib_path" wird später erläutert.

Rufen Sie auf jeden Fall "init" des resultierenden Rutie-Objekts auf. Das erste Argument, "Init_mymath", ist der Name dessen, was ich vorläufig als "Initialisierungsfunktion" bezeichnet habe. Das zweite Argument wird in Kürze angesprochen.

Auf diese Weise findet init die Bibliotheksdatei libmy_rutie_math.dylib, die Rutie verwenden sollte, und ruft die Funktion Init_mymath auf. Auch hier hängt die Erweiterung dieser Datei vom Ziel ab. Rutie denkt darüber nach und findet es.

Es ist also ein Ort, an dem man es findet, aber es ist ein wenig verwirrend. Erstens, basierend auf dem zweiten Argument von "init", ist ersichtlich, dass es durch den relativen Pfad verschoben wird, der "lib_path" gegeben ist.

Für diesen Artikel habe ich das Ruby-Skript in das Stammverzeichnis von Rusts Projekt eingefügt, sodass "__dir __" vorhanden ist. Die Datei befindet sich also in "target / release" von dort aus.

Wenn Sie "lib_path" oder eine andere Option nicht angeben, ist dies "../ target / release" ". In diesem Fall ist dies unpraktisch, daher habe ich "lib_path" angegeben.

verwenden

Einfach zu verwenden. Gemäß Codebeispiel. Das MyMath Modul hat eine singuläre Methode hypot3, also nenne es einfach normal. Zur Bestätigung wird auch "Math.sqrt (1 * 1 + 2 * 2 + 3 * 3)" angezeigt, aber der gleiche Wert wurde erhalten.

Es gibt jedoch eine Einschränkung. Es wurde (auf der Rost-Seite) entschieden, dass alle drei Argumente von "hypot3" "Float" sind. Was ist, wenn Sie "MyMath.hypot3" ein Integer-Objekt geben?

Ich versuchte es. Tot. Es ist die sogenannte Panik. Wenn Sie etwas anderes als Float füttern, sterben Sie bei x.unwrap (). Natürlich können Sie auf der Rost-Seite, anstatt plötzlich "unfrap ()", wenn Sie den Fall durch "Ok" und "Err" teilen, eine Funktion erstellen, die nicht stirbt. Alternativ gibt es auf der Ruby-Seite kein Problem, wenn Sie es aufrufen, indem Sie es in Float typisieren.

Prüfstandstest

Beim letzten Mal (Ruby / Rust-Verknüpfung (3) Numerische Berechnung mit FFI) habe ich "hypot3" auf eine Weise ausprobiert, die FFI direkt verwendet, und "Rust" Es war viel schneller, in Ruby zu schreiben als anzurufen. "

Wie wäre es mit der Rutie-Version? Lass es uns ohne große Erwartung tun.

Testcode

Auch dieses Mal werden wir mit einem Edelstein namens Benchmark_driver messen.

im Voraus

gem i benchmark_driver

Und installieren Sie es. (Es ist oft verwirrend, aber der Edelsteinname ist ein Unterstrich anstelle eines Bindestrichs.)

Dieses Mal werde ich, anders als beim letzten Mal, den Testcode in Ruby schreiben.

Beachten Sie, dass wir im obigen Beispielcode "dir" verwendet haben, um ** hier ** darzustellen. Wenn Sie es jedoch in Benchmark_Driver schreiben, wird es von Benchmark_Driver anstelle des Speicherorts des Benchmark-Programms generiert. Dies bedeutet, dass sich die temporäre Datei befindet und die Rust-Bibliothek nicht gefunden werden kann.

Der folgende Code hat das erfunden.

require "benchmark_driver"

Benchmark.driver do |r|
  r.prelude <<~EOT
    gem "rutie", "~> 0.0.4"
    require "rutie"

    Rutie.new(:my_rutie_math, lib_path: "target/release").init "Init_mymath", "#{__dir__}"
  EOT

  r.report "MyMath.hypot3(1.0, 2.0, 3.0)"
  r.report "Math.sqrt(1.0 * 1.0 + 2.0 * 2.0 + 3.0 * 3.0)"
end

Vorspiel schreibt, was vor der Messung zu tun ist. report schreibt den Prozess, den Sie messen möchten.

Testlauf

Wenn Sie das obige Skript ausführen:

Math.sqrt(1.0 * 1.0 + 2.0 * 2.0 + 3.0 * 3.0):  11796989.3 i/s
                MyMath.hypot3(1.0, 2.0, 3.0):   5684591.1 i/s - 2.08x  slower

Es ist eine schreckliche Niederlage. Die Ausführungsgeschwindigkeit der Rutie-Version ist fast dieselbe wie bei der vorherigen FFI-Version. Das Schreiben in Ruby dauert doppelt so lange.

Kann ich heute schlafen? Lassen Sie Rust als Nächstes die schwerere Verarbeitung durchführen und das Ruby-Skript anzeigen.

Recommended Posts

Ruby / Rust-Verknüpfung (4) Numerische Berechnung mit Rutie
Ruby / Rust-Kooperation (5) Numerische Berechnung mit Rutie ② Veggie
Ruby / Rust-Kooperation (3) Numerische Berechnung mit FFI
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
Rubin / Rost-Kooperation (2) Mittel
Evolve Eve mit Ruby