Coopération Ruby / Rust (5) Calcul numérique avec Rutie ② Veggie

Série d'articles

introduction

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.

Matière

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 ko </ rt> ambre haku </ rt> </ ruby>.

politique

Pour divers $ t , nous calculerons avec le même coefficient ( a_0 $, $ a_1 $, $ a_2 $, $ a_3 $), alors créons une classe. Donnez un coefficient pour créer une instance, puis donnez $ t $ pour calculer la valeur du polypole.

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.

Mise en œuvre: côté rouille

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.

Jusqu'à la modification de Cargo.toml

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

Corps

politique

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.

code

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.

Wrapper

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.

Définitions de classe et de méthode

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.

Définition de la fonction d'initialisation

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 trois neuf </ ruby> [^ g999].

[^ 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.

compiler

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.

Implémentation: côté rubis

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

Bonus: dessinons une image de la courbe de Bézier

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. bezier.png

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

Test sur banc

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.

Code de test

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

Coopération Ruby / Rust (5) Calcul numérique avec Rutie ② Veggie
Liaison Ruby / Rust (4) Calcul numérique avec Rutie
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