Série d'articles
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é.
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.
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.
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é.
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.
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.
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 que
pub` 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.
Dans le répertoire racine du projet
cargo build --release
Si vous le faites, il sera compilé.
build
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
.
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 que
ffi_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!
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!
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).
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.
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