Dernière fois via FFI
\sqrt{ x^2 + y^2 + z^2 }
J'ai appelé la fonction de Rust pour calculer à partir de Ruby. C'était une récolte qui s'est avérée étonnamment facile à mettre en œuvre, mais la vitesse clé est en Ruby.
Math.sqrt(x * x + y * y + z * z)
C'était dommage que ce soit beaucoup plus lent que de calculer. Je ne connais pas la cause, mais j'ai émis l'hypothèse que le coût du passage par FFI était élevé.
Alors pourquoi ne pas essayer de connecter Rust et Ruby par des moyens autres que FFI? Cet article utilise quelque chose appelé Rutie.
Qiita ne semble pas avoir d'article sur Rutie, donc je pense que c'est le premier article.
De plus, il convient de le noter.
Ça a été utilisé.
Site officiel: danielpclark / rutie: “The Tie Between Ruby and Rust.”
Rutie semble se lire comme "rooty".
Alors que FFI est un mécanisme à usage général qui connecte plusieurs langues, Rutie n'est qu'entre Ruby et Rust. Une grande fonctionnalité est que vous pouvez écrire des classes, des modules et des méthodes Ruby dans Rust. Rust a également des types correspondant à Ruby String, Array et Hash. De plus, il semble que les méthodes Ruby puissent être appelées du côté Rust.
Idem que pour FFI
\sqrt{ x^2 + y^2 + z^2 }
Ecrivez une fonction à calculer en Rust et appelez-la en Ruby.
Dans le cas de FFI, c'était comme attribuer une fonction Rust à une méthode Ruby, alors que dans le cas de Rutie, c'était comme écrire une méthode Ruby directement dans Rust.
Comme pour FFI, je vais l'écrire pour que même les personnes qui ne sont pas familières avec Rust puissent le reproduire. Cependant, on suppose que Rust a été installé.
Premier au terminal
cargo new my_rutie_math --lib
Et faites un projet Rust.
my_rutie_math
est le nom du projet. Un répertoire avec le même nom sera créé, dans lequel l'ensemble initial de fichiers sera stocké.
--lib
est une spécification selon laquelle" Je vais créer une caisse de bibliothèque ".
La fin de Cargo.toml dans la racine du projet se termine par «[dépendances]», qui est comme suit.
Cargo.toml
[dependencies]
rutie = "0.7.0"
[lib]
crate-type = ["cdylib"]
(Ajout 2020-10-01) La dernière version de rutie à partir du 2020-10-01 est 0.8.1, donc si vous voulez l'essayer à partir de maintenant
rutie = "0.8.1"
S'il vous plaît. [dependencies]
Où «[dépendances]» est, la caisse de dépendances est spécifiée. En Ruby, c'est comme spécifier une gemme dépendante dans un Gemfile.
Ici, il dit qu'il utilisera la caisse rutie.
"0.7.0" "est la spécification de version de la caisse rutie, mais cela ne signifie pas" en faire la version 0.7.0 "mais" version 0.7.0 ou plus et moins de 0.8.0 ".
En d'autres termes, c'est la même chose que de spécifier " ~> 0.7.0 "
dans le Ruby Gemfile.
Depuis le 4 septembre 2020, la dernière version de rutie crate était la version 0.8.0, mais pour une raison quelconque, la 0.8.0 ne fonctionnait pas [^ dame], donc je vais reporter l'enquête sur la cause et une version plus ancienne de 0.7. Continuez avec .0.
[^ dame]: Je l'ai essayé avec macOS et msvc et gnu de Windows, mais j'obtiens une erreur au stade du lien lors de la compilation. Je ne l'ai pas étudié en détail, mais si j'ai une chance, j'aimerais le résumer dans un autre article.
Veuillez noter que l'explication sur le site officiel est écrite sur la prémisse de 0.8.0.
(Ajout 2020-10-01) Après cela, lorsque j'ai essayé à nouveau la 0.8.0 sur macOS, il n'y avait pas de problème particulier. 0.8.1 était également correct. Peut-être que quelque chose a changé. S'il vous plaît laissez-moi savoir si vous obtenez une erreur dans la construction.
[lib]
Je ne suis pas sûr de ce que signifie le prochain «[lib]».
crate-type
spécifie littéralement le type de crate. De la caisse binaire et de la caisse de bibliothèque, la caisse de bibliothèque est créée, mais il semble qu'il existe en fait différents types de caisse de bibliothèque.
Les articles suivants sont utiles.
J'ai essayé de résumer le type de crate de Rust - Qiita
cdylib
semble signifier une bibliothèque dynamique pour d'autres langues (c'est-à-dire des langues autres que Rust).
Eh bien, est-ce que «dy» signifie dynamique et «c» signifie langage C?
Ensuite, la description du corps principal.
Rutie vous permet de créer des modules et des classes Ruby. Cette fois, je veux juste créer une fonction (méthode cible), alors faisons-en un module au lieu d'une classe. Le nom du module doit être «MyMath». Nommez la méthode «hypot3» et définissez-la comme une méthode singulière de «MyMath». La politique a été définie.
Créez le fichier src / lib.rs comme suit. À l'origine, le modèle de code de test est écrit, mais vous pouvez le supprimer.
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)
});
}
La quantité de description est légèrement supérieure à la version FFI.
Tout d'abord, faites attention au module! Et aux méthodes!. Ce sont des macros définies dans la caisse rutie, qui semblent définir les modules et méthodes Ruby.
Pour utiliser ces macros, au début
#[macro_use]
extern crate rutie;
(Je ne sais pas).
Object, Module, Float
Dans Rutie, il semble que les types de Rust correspondant à des classes telles que Object, Module, Class, Array, Float, Hash et Symbol of Ruby sont définis avec ** même nom **. Cependant, la Ruby String est nommée RString au lieu du même nom. Je suppose que j'ai ajouté «R» pour qu'il ne chevauche pas le String de Rust.
Cette fois, nous avons besoin de trois d'entre eux, Object, Module et Float.
use rutie::{Object, Module, Float};
J'écris ça.
Pour créer un module Ruby appelé MyMath
module!(MyMath);
Écrire.
Le module proprement dit n'est probablement pas créé ici, mais lorsque vous exécutez Module :: new (" MyMath ")
, qui apparaît plus tard.
La définition de méthode donne trois arguments à la macro methods!
.
Le premier argument est le nom du module «MyMath».
Je n'ai aucune idée du deuxième argument, _rtself
.
La définition de la fonction est donnée dans le troisième argument. Extrayons:
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())
}
Tout d'abord, le nom de la fonction est préfixé avec pub_
, ce qui est similaire à l'exemple de code sur le site Rutie, qui indique que" le nom de la fonction est préfixé avec pub_
afin qu'il ne se chevauche pas avec d'autres." Il n'est pas nécessaire de le fixer s'il est clair qu'il ne sera pas porté.
Soyez assuré que pub_hypot3
sera le nom de la méthode hypot3
du côté Ruby.
À propos, l'argument et la valeur de retour sont de type «Float» au lieu de «f64».
Float
semble être défini ici:
https://github.com/danielpclark/rutie/blob/v0.7.0/src/class/float.rs
Les commentaires écrits ici peuvent être trouvés dans la documentation ci-dessous: https://docs.rs/rutie/0.7.0/rutie/struct.Float.html
Une grande question s'est posée ici. Où convertir l'argument en f64
x.unwrap().to_f64()
J'essaie.
D'après le document précédent, Float
devrait pouvoir être converti en f64
avecto_f64 ()
.
Pourquoi mordez-vous ʻunwrap () `?
Salut, le type de ce «x» est
std::result::Result<rutie::Float, rutie::AnyException>
Semble être. C'est probablement "Result" car lorsque vous obtenez la valeur de Ruby, vous pouvez recevoir quelque chose d'étrange. Ainsi, ʻunwrap () récupérera une valeur de type
Float`.
Mais! Le type de fonction est
fn pub_hypot3(x: Float, y: Float, z: Float) -> Float
N'est-ce pas? C'est «Float», «x» est. Ah ~?
Je ne pouvais pas le découvrir par mes capacités même si je les recherchais.
Cependant, je pensais que le fait pourrait être que cette partie est un argument de la macro méthodes!
.
Oui, cette définition de fonction ** - comme chose ** est quelque chose qui est passé à la macro. Ce n'est pas la fonction Rust elle-même.
Laissons cette question de côté et passons à autre chose. Dans le corps de fonction
let x = x.unwrap().to_f64();
C'est dit. C'est ce qu'on appelle le shadowing qui définit «x» avec le même nom même s'il y a «x» dans l'argument. Le «x» d'origine n'est plus nécessaire, utilisez donc une variable du même nom.
Dernier
Float::new((x * x + y * y + z * z).sqrt())
Génère une valeur de type Float
à renvoyer du côté Ruby en fonction de la valeur f64
calculée.
Le terme «fonction d'initialisation» est quelque chose que j'ai trouvé et peut ne pas être approprié. Quoi qu'il en soit, définissez une fonction à appeler du côté Ruby. En faisant cela, je pense que les modules et méthodes Ruby définis dans Rust peuvent en fait être utilisés du côté Ruby.
Extrait ci-dessous.
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_mymath() {
Module::new("MyMath").define(|module| {
module.def_self("hypot3", pub_hypot3)
});
}
Je ne connais pas la convention de dénomination du nom de la fonction, mais je l'ai fait au format ʻInit_XXXXselon l'exemple de code. Le mot
# [allow (non_snake_case)]` au début signifie probablement que le compilateur est bloqué avec "Ne vous plaignez pas parce qu'il était intentionnel de ne pas en faire un cas de serpent."
# [no_mangle]
est un sort familier que la bibliothèque met lors de la définition d'une fonction à montrer à l'extérieur, et il semble que le nom de la fonction ne puisse pas être référencé par ce nom sans lui.
Quant au contenu de la fonction, il semble que le module MyMath
est d'abord créé avec Module :: new (" MyMath ")
, et la méthode est créée avec def_self
pour cela.
L'argument de define
est
|module| {
module.def_self("hypot3", pub_hypot3)
}
C'est sous la forme de. C'est ce qu'on appelle une fermeture.
Il est intéressant de noter que la syntaxe est très similaire aux blocs Ruby. La différence est que la partie correspondant au paramètre de bloc de Ruby est en dehors du {}
.
Les blocs Ruby ne sont pas des valeurs (pas des objets), mais les fermetures Rust sont des valeurs et peuvent être passées aux arguments de fonction.
module.def_self (" hypot3 ", pub_hypot3)
semble signifier que le pub_hypot3
précédemment défini est généré dans le module MyMath
comme une méthode nommée hypot3
.
Avec cela, l'implémentation du côté Rust est correcte. Il y avait des choses que je ne comprenais pas et c'était un peu déroutant. Mais ne serait-il pas bien si les classes, modules et méthodes Ruby pouvaient être définis avec ce niveau de complexité?
Dans le répertoire racine du projet
cargo build --release
Je le ferai.
L'artefact sera alors dans le chemin target / release / libmy_rutie_math.dylib
.
Cependant, l'extension doit être différente selon la cible. Je me demande si ce sera «.dll» sous Windows.
(Une addition) Lors de la compilation
warning: `extern` fn uses type `MyMath`, which is not FFI-safe
--> src/lib.rs:9:5
|
9 | MyMath,
| ^^^^^^ not FFI-safe
Est affiché. (J'ai écrit les versions Ruby et Rust au début de l'article)
Le nom «MyMath» semble dire «pas sûr FFI». Je ne sais pas ce que cela signifie, mais c'est un avertissement, pas une erreur, donc je vais l'ignorer pour l'instant.
(Ajout 2020-10-01) L'avertissement «not FFI-safe» est sorti dans Rust 1.46. Il a été résolu dans Rutie 0.8.1. https://github.com/danielpclark/rutie/issues/128
Du côté Ruby, utilisez une gemme appelée «rutie». Le même nom que la caisse utilisée dans Rust. Facile à comprendre.
L'exemple de script suivant est écrit en supposant qu'il existe dans le répertoire racine du projet Rust.
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
#référence
p Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)
# => 3.7416573867739413
Dans l'exemple ci-dessus, j'ai essayé de le montrer dans un fichier, et soudain j'ai fait gem" rutie "," ~> 0.0.4 "
, mais en général je l'écrirais dans Gemfile.
Maintenant, comment utiliser rutie gem, tout d'abord, il semble créer un objet Ruby avec Rutie.new
.
J'ai écrit : my_rutie_math
dans le premier argument, qui est le nom de la bibliothèque créée par Rust.
Dans cet article, le nom du projet donné lors de la première utilisation de cargo new
est utilisé comme nom de bibliothèque.
Mais à [lib]
dans Cargo.toml
Cargo.toml
[lib]
name = "hoge"
Si vous donnez name
comme ceci, ce devrait être le nom de la bibliothèque.
Et cela devrait être reflété dans le nom de fichier de l'artefact résultant.
L'argument optionnel lib_path
sera discuté plus tard.
Quoi qu'il en soit, appelez ʻinit` de l'objet Rutie résultant. Le premier argument, "" Init_mymath ", est le nom de ce que j'ai provisoirement appelé la" fonction d'initialisation ". Le deuxième argument sera abordé sous peu.
Quoi qu'il en soit, faire cela ʻinittrouve le fichier de bibliothèque
libmy_rutie_math.dylib que Rutie devrait utiliser et appelle la fonction ʻInit_mymath
.
Là encore, l'extension de ce fichier dépend de la cible.
Rutie y réfléchit et le trouve.
Donc, c'est un endroit pour le trouver, mais c'est un peu déroutant.
Premièrement, sur la base du deuxième argument de ʻinit, on voit qu'il est déplacé par le chemin relatif donné à
lib_path`.
Pour cet article, j'ai mis le script Ruby à la racine du projet de Rust, donc __dir __
est là.
Ainsi, le fichier se trouve dans target / release
vu de là.
Si vous ne donnez pas lib_path
ou toute autre option, ce sera" ../ target / release "
.
Dans ce cas, ce n'est pas pratique, j'ai donc spécifié lib_path
.
Facile à utiliser. Selon l'exemple de code.
Le module MyMath
a une méthode singulière hypot3
, alors appelez-la normalement.
Pour confirmation, Math.sqrt (1 * 1 + 2 * 2 + 3 * 3)
est également affiché, mais la même valeur a été obtenue.
Cependant, il y a une mise en garde.
Il a été décidé (du côté de Rust) que les trois arguments de «hypot3» sont «Float».
Que faire si vous donnez à MyMath.hypot3
un objet Integer?
Je l'ai essayé. Morte. C'est la soi-disant panique.
Si vous nourrissez autre chose que Float, vous mourrez à x.unwrap ()
.
Bien sûr, du côté de Rust, au lieu de soudainement ʻunwrap () , si vous divisez le cas par ʻOk
et ʻErr`, vous pouvez créer une fonction qui ne meurt pas.
Alternativement, du côté Ruby, il n'y a aucun problème si vous l'appelez en le transtypant en Float.
La dernière fois (Lien Ruby / Rust (3) Calcul numérique avec FFI), j'ai essayé hypot3
d'une manière qui utilise FFI directement, et" Rust C'était beaucoup plus rapide d'écrire en Ruby que d'appeler. "
Et la version Rutie? Faisons-le sans trop d'attentes.
Cette fois également, nous mesurerons à l'aide d'un joyau appelé benchmark_driver.
en avance
gem i benchmark_driver
Et installez-le. (C'est souvent déroutant, mais le nom de la gemme est un trait de soulignement au lieu d'un trait d'union)
Cette fois, contrairement à la dernière fois, j'écrirai le code de test en Ruby.
Une chose à garder à l'esprit est que dans l'exemple de code ci-dessus, nous avons utilisé __dir__
pour représenter ** ici **, mais si vous l'écrivez dans benchmark_driver, il sera généré par benchmark_driver au lieu de l'emplacement du programme de référence. Cela signifie que le fichier temporaire est localisé et que la bibliothèque Rust est introuvable.
Le code ci-dessous a conçu cela.
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
«Prelude» écrit ce qu'il faut faire avant la mesure.
report
écrit le processus que vous souhaitez mesurer.
Lorsque vous exécutez le script ci-dessus:
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
C'est une terrible défaite. La vitesse d'exécution de la version Rutie est presque la même que celle de la version FFI précédente. Cela prend deux fois plus de temps que d'écrire en Ruby.
Euh, je peux dormir aujourd'hui? Ensuite, laissez Rust faire le traitement plus lourd et révéler le script Ruby.
Recommended Posts