Liaison Ruby / Rust (4) Calcul numérique avec Rutie

introduction

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é.

Qu'est-ce que Rutie

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.

Matière

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.

Mise en œuvre: côté rouille

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é.

Création de projet

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 ".

Modification de Cargo.toml

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?

Description du module et de la méthode

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.

macro

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.

Définition du module

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.

Définition de la méthode

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.

Définition de la fonction d'initialisation

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é?

compiler

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

Implémentation: côté rubis

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èquelibmy_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.

utilisation

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.

Test sur banc

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.

Code de test

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.

Essai

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

Liaison Ruby / Rust (4) Calcul numérique avec Rutie
Coopération Ruby / Rust (5) Calcul numérique avec Rutie ② Veggie
Coopération Ruby / Rust (3) Calcul numérique avec FFI
Coopération rubis / rouille (6) Extraction de morphologie
Calcul numérique orienté objet
Calcul numérique conditionnel
Coopération rubis / rouille (1) Objectif
Programme de calcul du score Ruby
Premiers pas avec Ruby
Coopération Rubis / Rouille (2) Moyens
Evolve Eve avec Ruby