Série d'articles
La dernière fois, j'ai utilisé Rutie, qui relie Ruby et Rust, pour appeler une simple fonction de calcul numérique de Rust de Ruby. En termes de vitesse, c'était beaucoup plus lent que de laisser Ruby faire le même calcul. C'est probablement parce que le coût de l'appel des fonctions Rust depuis Ruby était raisonnable. Comparé au coût, le calcul numérique que j'ai dû faire était trop léger.
Encore une fois, en utilisant Rutie, appelons Rust de Ruby pour faire des calculs numériques. Qu'est-ce qui est différent de la dernière fois
Le lieu. En particulier, ce dernier est important, et si cela peut être fait, les choses qui peuvent être faites en collaboration avec Rust s'élargiront considérablement.
Je veux calculer la courbe de Bézier. Référence: [Begje Curve-Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%99%E3%82%B8%E3%82%A7%E6%9B%B2%E7%B7% 9A)
Pour un bezier cubique, donnant quatre points sur le plan $ \ boldsymbol {p} _0 $, $ \ boldsymbol {p} _1 $, $ \ boldsymbol {p} _2 $, $ \ boldsymbol {p} _3 $ La courbe est décidée. Cette courbe est affichée comme une variable médiatrice comme suit.
\boldsymbol{p}(t) = (1-t)^3 \boldsymbol{p}_0 + 3t(1-t)^2 \boldsymbol{p}_1 + 3t^2(1-t) \boldsymbol{p}_2 + t^3 \boldsymbol{p}_3 \quad\quad (0 \leqq t \leqq 1)
Comme vous pouvez le voir immédiatement, lorsque $ t = 0 $, c'est $ \ boldsymbol p_0 $, et lorsque $ t = 1 $, c'est $ \ boldsymbol p_3 $. En d'autres termes, c'est une courbe qui commence à $ \ boldsymbol p_0 $ et se termine à $ \ boldsymbol p_3 $. $ \ Boldsymbol {p} _1 $ et $ \ boldsymbol {p} _2 $ ne passent généralement pas (peut passer selon les conditions).
$ \ Boldsymbol p_1 $ et $ \ boldsymbol p_2 $ sont aussi appelés points de contrôle, $ \ boldsymbol p_1- \ boldsymbol p_0 $ est un vecteur tangent à $ t = 0 $, et $ \ boldsymbol p_3- \ boldsymbol p_2 $ est C'est un vecteur tangent à $ t = 1 $.
Maintenant, ce que je veux faire est la position $ \ boldsymbol p (t) $ à tout $ t $ donné $ \ boldsymbol p_0 $, $ \ boldsymbol p_1 $, $ \ boldsymbol p_2 $, $ \ boldsymbol p_3 $. Obtenir.
Puisque les coordonnées $ x $ et $ y $ ne sont pas liées l'une à l'autre, pour $ a_0 $, $ a_1 $, $ a_2 $, $ a_3 $,
B(t) = (1-t)^3 a_0 + 3t(1-t)^2 a_1 + 3t^2(1-t) a_2 + t^3 a_3
Vous pouvez penser à une fonction du formulaire. Préparez-le respectivement pour les coordonnées $ x $ et $ y $.
Cette fonction est appelée polynôme de Bernstein de degré [^ bern]. En d'autres termes, le thème cette fois est "Calculer la valeur du polynôme de Bernstein de degré 3".
[^ bern]: Le nom de ce polymorphe est parfois écrit en anglais "Bernstein polyploid" ou en allemand "Bernsch ** ta ** in polypoly", mais il est nommé d'après l'ancien mathématicien soviétique Бернштейн Par conséquent, il a été défini comme "Bernsch ** te ** en polypoly" dans le style russe. Wikipédia [Sergei Bernstein](https://ja.wikipedia.org/wiki/%E3%82%BB%E3%83%AB%E3%82%B2%E3%82%A4%E3%83 % BB% E3% 83% 99% E3% 83% AB% E3% 83% B3% E3% 82% B7% E3% 83% A5% E3% 83% 86% E3% 82% A4% E3% 83% B3 ) Dit qu'il est né à Odessa, une ville face à la mer Noire. C'est actuellement la République d'Ukraine, mais il semble que c'était l'Empire russe à cette époque. Ce nom de famille est en Idish (juif allemand) et signifie ambre
Pour divers $ t
Appelons la classe CubicBezier. Non, ce que je fais, c'est calculer le polynôme de Bernstein, donc CubicBernstein est peut-être plus adapté au contenu, mais "Bézier" est mieux.
Lorsqu'il est implémenté dans Ruby,
class CubicBezier
def initialize(a0, a1, a2, a3)
@a0, @a1, @a2, @a3 = a0, a1, a2, a3
end
def value_at(t)
s = 1 - t
@a0 * s * s * s + 3 * @a1 * t * s * s + 3 * @a2 * t * t * s + @a3 * t * t * t
end
alias [] value_at
end
C'est comme ressentir
La raison pour laquelle l'alias «[]» est appliqué à «value_at» est que cela ressemble plus à Ruby de calculer avec «[]».
Quoi qu'il en soit, je vais implémenter une classe avec la même fonction dans Rutie.
Comment implémenter une classe dans Rutie qui a des variables d'instance. Heureusement, le code de Rutie a des explications et des exemples, alors j'ai essayé et fait des erreurs en les regardant, et j'ai trouvé quelque chose qui fonctionnait. Je ne comprends pas la théorie.
Pareil qu'avant
cargo new cubic_bezier --lib
Je le ferai. Et sur Cargo.toml
Cargo.toml
[dependencies]
lazy_static = "1.4.0"
rutie = "0.8.1"
[lib]
crate-type = ["cdylib"]
Mettez dedans. Cette fois, nous avons besoin d'une caisse lazy_static.
(Ajout 2020-10-01) La version de Rutie a été réglée sur «" 0.7.0 "», mais elle a été changée pour la dernière version «" 0.8.1 "». Cela élimine l'avertissement concernant le nom CubicBezier
lors de la compilation avec Rust 1.46. Faites-moi savoir s'il y a une personne qui dit "J'ai pu compiler avec 0.7.0 mais pas avec 0.8.1".
Je ne suis pas sûr du tout, mais lors de la définition d'une classe Ruby qui utilise des variables d'instance avec Rutie, il semble que la méthode consiste à préparer une structure Rust (struct) et à l'envelopper (envelopper ici J'écris sans savoir ce que ça veut dire).
Dans ce cas, nous voulons créer une classe Ruby appelée CubicBezier qui a des variables d'instance ʻa0, ʻa1
, ʻa2 et ʻa3
, donc nous définissons d'abord une structure avec de tels champs. Si le nom de la structure est CubicBezier, elle sera débordée, donc je n'ai pas d'autre choix que d'utiliser RustCubicBezier.
Définissez CubicBezier pour l'envelopper.
Voici tout le code.
src/lib.rs
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rutie;
use rutie::{Object, Class, Float};
pub struct RustCubicBezier {
a0: f64,
a1: f64,
a2: f64,
a3: f64,
}
impl RustCubicBezier {
pub fn value_at(&self, t: f64) -> f64 {
let s = 1.0 - t;
self.a0 * s * s * s + 3.0 * self.a1 * t * s * s + 3.0 * self.a2 * t * t * s + self.a3 * t * t * t
}
}
wrappable_struct!(RustCubicBezier, CubicBezierWrapper, CUBIC_BEZIER_WRAPPER);
class!(CubicBezier);
methods!(
CubicBezier,
rtself,
fn cubic_bezier_new(a0: Float, a1: Float, a2: Float, a3: Float) -> CubicBezier {
let a0 = a0.unwrap().to_f64();
let a1 = a1.unwrap().to_f64();
let a2 = a2.unwrap().to_f64();
let a3 = a3.unwrap().to_f64();
let rcb = RustCubicBezier{a0: a0, a1: a1, a2: a2, a3: a3};
Class::from_existing("CubicBezier").wrap_data(rcb, &*CUBIC_BEZIER_WRAPPER)
}
fn value_at(t: Float) -> Float {
let t = t.unwrap().to_f64();
Float::new(rtself.get_data(&*CUBIC_BEZIER_WRAPPER).value_at(t))
}
);
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_cubic_bezier() {
Class::new("CubicBezier", None).define(|klass| {
klass.def_self("new", cubic_bezier_new);
klass.def("value_at", value_at);
klass.def("[]", value_at);
});
}
Des explications seront ajoutées à chaque partie dans les sections suivantes.
RustCubicBezier
Définition des structures et de leurs méthodes:
pub struct RustCubicBezier {
a0: f64,
a1: f64,
a2: f64,
a3: f64,
}
impl RustCubicBezier {
pub fn value_at(&self, t: f64) -> f64 {
let s = 1.0 - t;
self.a0 * s * s * s + 3.0 * self.a1 * t * s * s + 3.0 * self.a2 * t * t * s + self.a3 * t * t * t
}
}
Je ne pense pas que la définition de «RustCubicBezier» nécessite beaucoup d'explications.
Les fonctions Rust fonctionnent comme des méthodes lorsque le premier argument est défini comme & self
.
C'est la partie que je ne comprends pas vraiment.
wrappable_struct!(RustCubicBezier, CubicBezierWrapper, CUBIC_BEZIER_WRAPPER);
Il semble montrer la relation entre la structure RustCubicBezier
définie précédemment et le wrapper.
La documentation de la macro wrappable_struct!
Est ici:
rutie :: wrappable_struct --Rust (version Rutie 0.7.0)
(Ajout 2020-10-01) La dernière version de Rutie pour le moment est la 0.8.1, mais pour une raison quelconque, la version 0.8 n'a pas réussi à générer des documents Je laisse le lien vers le document dans la version 0.7.0 car la page n'existe pas.
J'ai l'impression que cela dit en quelque sorte que la structure de Rust peut être enveloppée d'un objet Ruby (je ne suis pas bon en anglais).
Le premier argument semble donner le nom de la structure Rust que vous souhaitez envelopper. Il semble que cette structure doit être publique, j'ai donc ajouté pub
lorsque je l'ai défini plus tôt.
Le deuxième argument semble être le nom de la structure (wrapper) pour encapsuler le premier argument. Cette structure est automatiquement définie par la macro. Cependant, dans ce code, le CubicBezierWrapper
donné comme deuxième argument n'apparaît nulle part ailleurs.
Le troisième argument est le nom de la variable statique [^ contain] qui contient le wrapper.
[^ contain]: Puisqu'il était "contenir" dans le texte original, il était "inclus", mais cela signifie-t-il simplement "avoir comme valeur" (~ est assigné)?
♪ Ingénieur senior qui a arrêté d'apprendre
♪ Tu es mort
♪ Dernière chance d'apprendre passionnant
♪ Répéter la tranche Rust Chunk
♪ Gronde-moi, compilateur démon
♪ Ma tête est déjà en difficulté
Non, ce n'est pas le cas avec les wrappers [^ wrapper].
[^ wrapper]: Je ne connais pas le hip hop, qui n'est pas familier avec la musique, donc je ne sais pas si les paroles du rap sont comme ça, mais j'ai écrit les paroles de Tekito.
Première classe. C'est simple.
class!(CubicBezier);
Puis la méthode.
methods!(
CubicBezier,
rtself,
fn cubic_bezier_new(a0: Float, a1: Float, a2: Float, a3: Float) -> CubicBezier {
let a0 = a0.unwrap().to_f64();
let a1 = a1.unwrap().to_f64();
let a2 = a2.unwrap().to_f64();
let a3 = a3.unwrap().to_f64();
let rcb = RustCubicBezier{a0: a0, a1: a1, a2: a2, a3: a3};
Class::from_existing("CubicBezier").wrap_data(rcb, &*CUBIC_BEZIER_WRAPPER)
}
fn value_at(t: Float) -> Float {
let t = t.unwrap().to_f64();
Float::new(rtself.get_data(&*CUBIC_BEZIER_WRAPPER).value_at(t))
}
);
Cliquez ici pour la définition de la macro méthodes!
:
https://docs.rs/rutie/0.7.0/src/rutie/dsl.rs.html#356-398
La signification du deuxième argument de la macro «méthodes!», Que je n'ai pas comprise la dernière fois, semblait être vaguement comprise (décrite plus loin).
Deux méthodes sont définies ici.
cubic_bezier_new
crée une instance (cela devient nouveau
).
value_at
calcule la valeur de la fonction de Bernstein pour t
.
Comme je l'ai écrit la dernière fois, cette définition de fonction est un argument de la macro méthodes!
, Et ce n'est pas une fonction de Rust telle quelle. Cela devient une définition de fonction de Rust par la fonction de la macro, mais à ce moment-là, il fait gonyogonyo.
Si vous êtes familier avec les macros Rust, vous pouvez probablement le comprendre en regardant le lien ci-dessus.
Dans cubic_bezier_new
, une structure de type RustCubicBezier
à encapsuler est générée en fonction de l'argument.
De la dernière ligne
Class::from_existing("CubicBezier").wrap_data(rcb, &*CUBIC_BEZIER_WRAPPER)
Mais je n'en suis pas sûr non plus.
Class :: from_existing (" CubicBezier ")
semble obtenir la classe CubicBezier
en bref. Est-ce quelque chose comme const_get (" CubicBezier ")
en Ruby?
La documentation pour wrap_data
est ici (lue un jour):
rutie::Class - Rust
value_at
est beaucoup plus facile à comprendre.
Le foie
rtself.get_data(&*CUBIC_BEZIER_WRAPPER)
Au fait. À ce stade, le deuxième argument rtself
des méthodes!
Macro est finalement sorti.
Cette expression semble renvoyer une structure RustCubicBezier enveloppée.
rtself
est probablement un rôle de Ruby.
La même note que la dernière fois. La "fonction d'initialisation" est provisoirement nommée par moi et peut ne pas être appropriée.
#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_cubic_bezier() {
Class::new("CubicBezier", None).define(|klass| {
klass.def_self("new", cubic_bezier_new);
klass.def("value_at", value_at);
klass.def("[]", value_at);
});
}
ʻ Définit une fonction qui peut être appelée de l'extérieur, nommée Init_cubic_bezier`. Peut-être qu'en faisant cela, vous pouvez créer des classes et des méthodes Ruby.
Il semble utiliser def_self
pour les méthodes de classe (c'est la même chose que la dernière fois) et def
pour les méthodes d'instance.
Pour «value_at» du côté Rust, «value_at» et «[]» sont affectés du côté Ruby. Désormais, CubicBezier # value_at
et CubicBezier # []
sont comme des alias.
Hmmm, il y a beaucoup de choses que je ne comprends pas, mais j'ai réussi à obtenir le code côté Rust en me référant à l'exemple de code.
Le code qui "ne comprend pas le principe mais fonctionne en combinant quelque chose qui peut être utilisé d'une manière ou d'une autre" est comme une locomotive 999
[^ g999]: La locomotive du Galaxy Super Express 999 qui apparaît dans "Galaxy Railroad 999" de Reiji Matsumoto est une technologie par une civilisation inconnue découverte des ruines de l'espace (je ne connais pas le contenu, mais je peux l'utiliser d'une manière ou d'une autre ) Est combiné. Cela doit avoir été un tel cadre.
Dans le répertoire racine du projet
cargo build --release
Ensuite, vous pouvez créer target / release / libmy_rutie_math.dylib
. Cependant, l'extension est probablement «.so» sous Linux et probablement «.dll» sous Windows («.dylib» est pour macOS).
Ce fichier est le seul utilisé du côté Ruby.
Le code du côté Rust était un peu déroutant, tandis que le code du côté Ruby était assez simple.
Comme précédemment, le code suivant existe dans le répertoire racine du projet Rust.
(Sinon, prenez le deuxième argument de la méthode ʻinit(ou le
lib_path de
Rutie.new`) selon le cas.
require "rutie"
Rutie.new(:cubic_bezier, lib_path: "target/release").init "Init_cubic_bezier", __dir__
cb = CubicBezier.new(1.0, 2.0, 1.5, 0.0)
0.0.step(1, by: 0.1) do |t|
puts cb[t]
end
Vous pouvez maintenant calculer le bezier cubique en Ruby.
Eh bien, si vous voulez l'utiliser correctement, ne le portez pas sur le côté comme ci-dessus, mais mettez-le dans Gemfile
Gemfile
gem "rutie", "~> 0.0.4"
Écrivez quelque chose comme Bundle.require
.
Maintenant que vous pouvez calculer la courbe de Bézier, dessinons une image. Utilisez Cairo.
require "rutie"
require "cairo"
Rutie.new(:cubic_bezier, lib_path: "target/release").init "Init_cubic_bezier", __dir__
size = 400
surface = Cairo::ImageSurface.new Cairo::FORMAT_RGB24, size, size
context = Cairo::Context.new surface
context.rectangle 0, 0, size, size
context.set_source_color :white
context.fill
points = [[50, 100], [100, 300], [300, 350], [350, 50]]
bezier_x = CubicBezier.new(*points.map{ |x, _| x.to_f })
bezier_y = CubicBezier.new(*points.map{ |_, y| y.to_f })
context.set_source_color :gray
context.set_line_width 2
context.move_to(*points[0])
context.line_to(*points[1])
context.move_to(*points[2])
context.line_to(*points[3])
context.stroke
n = 100 #Numéro de division
context.set_source_color :orange
(1...n).each do |i|
t = i.fdiv(n)
context.circle bezier_x[t], bezier_y[t], 1.5
context.fill
end
context.set_source_color :red
points.each do |x, y|
context.circle x, y, 4
context.fill
end
surface.write_to_png "bezier.png "
L'explication est omise (les questions sont les bienvenues). J'ai fait une photo comme celle-ci.
Les points rouges sont les quatre points qui définissent la courbe de Bézier cubique. La ligne grise est le vecteur tangent. Les petits points orange sont les points sur la courbe de Bézier calculés par CubicBezier # []
.
En regardant ce tableau de points orange, vous pouvez voir que "Oh, je pense que je peux le calculer correctement."
Il est maintenant temps pour le test de référence. En premier lieu, l'un des objectifs de cette tentative était de trouver un exemple d'accélération avec Rust.
J'utiliserai à nouveau benchmark_driver, donc si vous ne l'avez pas encore installé
gem i benchmark_driver
Installez avec.
require "benchmark_driver"
Benchmark.driver do |r|
r.prelude <<~PRELUDE
require "rutie"
Rutie.new(:cubic_bezier, lib_path: "target/release").init "Init_cubic_bezier", "#{__dir__}"
class RubyCubicBezier
def initialize(x0, x1, x2, x3)
@x0, @x1, @x2, @x3 = x0, x1, x2, x3
end
def [](t)
s = 1.0 - t
@x0 * s * s * s + 3.0 * @x1 * t * s * s + 3.0 * @x2 * t * t * s + @x3 * t * t * t
end
end
xs = [0.12, 0.48, 0.81, 0.95]
rust_cubic_bezier = CubicBezier.new(*xs)
ruby_cubic_bezier = RubyCubicBezier.new(*xs)
PRELUDE
r.report "rust_cubic_bezier[0.78]"
r.report "ruby_cubic_bezier[0.78]"
end
Exécutez ceci et comparez l'implémentation Ruby avec l'implémentation Rust. Pour vous dire la vérité, il y a de fortes chances que la version Rust soit plus lente en raison du coût de l'appel de Rust depuis Ruby.
Eh bien, le résultat est:
rust_cubic_bezier[0.78]: 6731741.2 i/s
ruby_cubic_bezier[0.78]: 4733084.6 i/s - 1.42x slower
Ou, j'ai gagné, la version Rust a gagné! Hyahoi! !!
Eh bien, c'est environ 1,4 fois plus rapide, donc ce n'est pas un gros problème. Pour être clair. Cependant, je pense qu'il est significatif de montrer que même une telle fonction (relativement simple) peut être accélérée avec Rust. J'espérais aussi que cela pourrait être réalisé avec des dizaines de lignes de code Rust.
À l'avenir, j'aimerais explorer l'aspect pratique en laissant Rust faire plus de processus divers.
Recommended Posts