Coopération Ruby / Rust (3) Calcul numérique avec FFI

Série d'articles

introduction

Essayons d'appeler une fonction Rust qui effectue des calculs numériques simples à partir de Ruby en utilisant FFI (Foreign Function Interface). Il y a déjà plusieurs articles avec un tel thème, c'est donc la Nième bière.

De plus, il convient de le noter.

Ça a été utilisé.

Matière

Quoi qu'il en soit, je veux faire quelque chose de plus pratique que le nombre de Fibonacci. Très bien, faisons ça. Il s'agit d'une version à 3 variables de Math. # Hypot. Cela peut être appelé une version tridimensionnelle.

La fonction de module étrangement nommée Math.hypot est fondamentalement

\sqrt{ x^2 + y^2 }

Quelque chose qui calcule. Il porte ce nom car il calcule la longueur de l'hypoténuse à partir des longueurs de deux côtés orthogonaux d'un triangle rectangle, $ x $ et $ y $.

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

La version à trois variables de ceci est, en bref

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

C'est une fonction qui calcule.

Motivation

Expliquez pourquoi vous voulez une version à 3 variables de Math.hypot. Cela n'a rien à voir avec le sujet de l'article, vous pouvez donc passer à la section suivante.

Cette fonction peut être utilisée pour obtenir la distance entre deux points donnés deux coordonnées sur le plan. En d'autres termes

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

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

Etc. Eh bien, si vous utilisez Vector

require "matrix"

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

distance = (p1 - p2).norm

Je peux y aller, cependant.

L'histoire a mal tourné.

Ensuite, en ce qui concerne la distance entre deux points dans un espace tridimensionnel, nous voulons naturellement une version tridimensionnelle de «hypot». Non, bien sûr, l'utilisation de «Vector» facilite l'écriture d'un nombre quelconque de dimensions comme décrit ci-dessus, mais il peut arriver que vous ne souhaitiez pas utiliser «Vector» pour des raisons de vitesse ou pour d'autres raisons.

3 Version variable

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

Peut être facilement défini. À propos, la raison d'utiliser «x * x» au lieu de «x ** 2» est que le premier est très lent [^ square].

[^ square]: Avec la récente optimisation des carrés, il est probable que x ** 2 ne sera pas trop lent autour de Ruby 3.0.

Cependant, la méthode ci-dessus a 3 multiplications, 2 additions et 1 sqrt, et comme ce sont tous des appels de méthode, les méthodes sont appelées un total de 6 fois. Ces expressions peuvent être plus rapides si elles sont réécrites en Rust ou C.

Mise en œuvre: côté rouille

Je vais l'écrire pour que même les personnes qui ne connaissent pas Rust puissent le reproduire. Cependant, on suppose que Rust a été installé.

Création de projet

Premier au terminal

cargo new my_ffi_math --lib

Et faites un projet Rust. my_ffi_math est le nom du projet. L'option --lib spécifie que" crée une caisse de bibliothèque ". Une caisse est une unité de compilation,

Il existe deux types.

Modification de Cargo.toml

Il existe un fichier appelé Cargo.toml à la racine du projet. Différents paramètres liés à l'ensemble du projet sont écrits ici. À la fin de ce fichier

Cargo.toml


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

Je vais ajouter. La signification de ceci n'est pas bien comprise par l'auteur.

Créer une fonction

Il devrait y avoir un fichier appelé src / lib.rs. Le modèle de code de test est écrit ici [^ test], mais vous pouvez le supprimer.

[^ test]: Rust vous permet d'écrire du code de test dans le code, et exécuter le test est très simple, faites simplement cargo test.

Et

src/lib.rs


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

Écrire.

Un mot-clé où «fn» représente la définition d'une fonction. pub et ʻexternsont des bonus pour ça, euh, je ne peux pas les expliquer correctement. Je pense quepub` est quelque chose comme" Je vais exposer cette fonction au monde extérieur ".

f64 représente un type appelé nombre à virgule flottante 64 bits, qui correspond à Ruby's Float via FFI. -> représente le type de retour de la fonction. Le contenu de la fonction peut être compris en le regardant. Avant la définition de la fonction

#[no_mangle]

Je suis curieux. Je ne suis pas sûr, mais si je n'écris pas ceci, il semble que même si j'ai défini la fonction avec le nom hypot3, je ne peux pas y faire référence par ce nom après la compilation.

C'est tout pour l'implémentation côté Rust.

compiler

Dans le répertoire racine du projet

cargo build --release

Si vous le faites, il sera compilé. build build </ ruby> est un moyen sympa de compiler (? Non, peut-être pas) Il y a deux façons de construire, une pour le débogage et une pour la publication (production), et --release signifie littéralement construire pour la publication. La vitesse d'exécution est lente pour le débogage.

La version compilée doit être dans le chemin target / release / libmy_ffi_math.dylib. Oh non, l'extension du nom de fichier est Iloilo. Quand je l'ai fait sur macOS, il est devenu .dylib, mais cela devrait être différent sous Windows [^ libext].

[^ libext]: Pour être précis, je pense que l'extension du produit ne dépend pas du système d'exploitation sur lequel il a été compilé, mais de la cible sur laquelle il a été compilé. En d'autres termes, si vous le compilez pour macOS (x86_64-apple-darwin) sous Windows, ce sera .dylib. Rust est facile à croiser.

C'est le seul fichier dont Ruby a besoin.

Dans ce cas, le nom de base du nom de fichier (la partie excluant l'extension) est le nom du projet avec lib au début. Si vous ajoutez name à` [lib] ʻin Cargo.toml

Cargo.toml


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

Le nom du fichier doit ressembler à libhoge.dylib.

Implémentation: côté rubis

Du côté Ruby, utilisez le gem ffi (vous pouvez également utiliser fiddle).

Facile avec le code ci-dessous

require "ffi"

Mais bien sûr si vous le gérez avec Gemfile

Gemfile


gem "ffi", "~> 1.13"

Écrivez-le dans un script

require "bundler"
Bundler.require

Etc.

Supposons également que le code Ruby se trouve pour le moment dans le répertoire racine du projet Rust.

Écrivez comme ça.

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

#référence
p Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)
# => 3.7416573867739413

Le résultat de l'exécution est également écrit dans le commentaire, mais on peut voir que le résultat correspond au calcul dans Ruby.

Peu importe ce que vous définissez comme FFIMath, je viens donc de préparer un joli module. ʻExend FFI :: Librarypour ce module. Cela donne lieu à des méthodes singulières telles queffi_lib`.

La méthode ffi_lib spécifie le chemin du fichier de bibliothèque compilé. Eh bien, je ne suis pas sûr, mais il semble que les chemins relatifs peuvent ne pas fonctionner, il semble donc préférable de donner un chemin absolu. Pour ce faire, utilisez File.expand_path

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

Et. Si vous écrivez comme ceci, le chemin relatif depuis l'emplacement de ce fichier (__dir__) sera un chemin absolu.

ʻAttach_functioncrée une fonction créée par Rust en tant que méthode singulière du module. Le premier argument est le nom de la fonction. Le deuxième argument spécifie le type d'argument de fonction. Puisqu'il a 3 arguments, c'est un tableau de longueur 3.: double` représente un nombre à virgule flottante double précision dans FFI. Cela correspond à f64 dans Rust et Float dans Ruby. Le troisième argument spécifie le type de retour de la fonction. Avec ce qui précède, la méthode singulière du module peut être définie.

Comme vous pouvez voir comment l'utiliser. Si vous avez une bonne idée, vous vous demandez peut-être, "Hmm? Vous devez donner Float à l'argument, mais vous donnez Integer?" Je ne suis pas familier avec cela, mais je suis sûr que ffi gem l'a converti en nombre à virgule flottante.

C'était étonnamment facile!

Test sur banc

J'espérais cette vitesse quand j'ai essayé d'implémenter hypot3 dans Rust. Ensuite, vous devez le prouver par un test de référence. Eh bien, à quelle vitesse ce sera!

Bibliothèque de tests

Lors de la mesure d'un traitement de la lumière tel que «hypot3», je pense que la bibliothèque de tests de référence est benchmark_driver.

Afin de mesurer le traitement de la lumière, il est nécessaire de mesurer le temps où le même traitement est exécuté plusieurs fois, mais si vous exécutez une boucle avec la méthode `` times '', etc., le coût de la boucle est relativement non négligeable et ne peut pas être mesuré avec précision. .. Il semble que benchmark_driver puisse mesurer la vitesse d'exécution réelle car il s'exécute plusieurs fois sans encourir un tel coût (je ne sais pas comment cela fonctionne).

Code de test

Comment utiliser benchmark_driver

Il y a deux façons, mais cette fois je vais essayer la dernière. Ce dernier vous oblige à vous souvenir du format YAML, mais ce n'est pas si difficile.

Écrivez comme suit.

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)

Pour le contenu de prélude, écrivez quelque chose qui doit être fait avant la mesure. J'ai défini Math.hypot3 pour comparaison.

Essai

Si vous pouvez le faire, au terminal

benchmark-driver benchmark.yaml

Et. (Il est déroutant que le nom de la commande soit un trait d'union même si le nom de la gemme est un trait de soulignement) Oh, installez la gemme benchmark_driver

gem i benchmark_driver

Faisons-le à l'avance.

Le résultat est ···

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

hein?

«Math.hypot3» peut s'exécuter 10 millions de fois par seconde, tandis que «FFI Math.hypot3» peut s'exécuter 5 millions de fois par seconde. N'est-ce pas une terrible défaite? Loin d'être rapide, c'est trop lent pour parler de [^ osoi].

[^ osoi]: Au fait, dans ce code, les objets Integer sont donnés à «x», «y», «z», mais quand un objet Float est donné, «Math.hypot3» ralentit de plus de 10%. «FFIMath.hypot3» était inchangé.

Quelle est la cause de la défaite? Le code de Rust ne semble pas s'améliorer davantage. Pour la compilation, j'ai spécifié correctement la version de la version. En regardant divers benchmarks de Rust, il semble qu'il ne soit pas inférieur à C, il ne semble donc pas que Rust soit lent.

Quand il s'agit de cela, n'est-ce pas le coût du FFI? Puisque Ruby peut passer n'importe quoi comme argument, je soupçonne que ffi gem effectue la vérification de type et la conversion au moment de l'exécution. Un tel traitement supplémentaire (?) Peut être un fardeau.

Il s'est avéré que l'implémentation du traitement d'environ hypot3 dans Rust ne semble pas utile. Nous devons faire un traitement plus lourd.

Jetons un second regard et pensons à quelque chose de "traitement plus lourd".

Recommended Posts

Coopération Ruby / Rust (3) Calcul numérique avec FFI
Liaison Ruby / Rust (4) Calcul numérique avec Rutie
Coopération Ruby / Rust (5) Calcul numérique avec Rutie ② Veggie
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
Sortie de calcul des calories rubis
Coopération Rubis / Rouille (2) Moyens
Evolve Eve avec Ruby