Series of articles table of contents
Let's try calling a Rust function that performs simple numerical calculations from Ruby using FFI (Foreign Function Interface). There are already multiple articles with such a theme, so it's the Nth brew.
In addition, it should be noted.
It was used.
Anyway, I want to make something more practical than the Fibonacci number. Alright, let's do that. It is a 3-variable version of Math. # Hypot. It can be called a three-dimensional version.
The strangely named module function Math.hypot
is, in essence,
\sqrt{ x^2 + y^2 }
The one that calculates. It has this name because it calculates the length of the hypotenuse from the lengths of two orthogonal sides of a right triangle, $ x $ and $ y $.
p Math.hypot(3, 4) # => 5.0
The three-variable version of this is, in short
\sqrt{ x^2 + y^2 + z^2 }
It is a function that calculates.
Talk about why you want a 3-variable version of Math.hypot
. It has nothing to do with the subject of the article, so you can skip to the next section.
This function can be used to get the distance between two points given two coordinates on a plane. In other words
p1 = [1, 3]
p2 = [2, -4]
distance = Math.hypot(p1[0] - p2[0], p1[1] - p2[1])
And so on. Well, if you use Vector
require "matrix"
p1 = Vector[1, 3]
p2 = Vector[2, -4]
distance = (p1 - p2).norm
I can go, though.
The story went awry.
Then, when it comes to the distance between two points in 3D space, we naturally want a 3D version of hypot
.
No, of course, using Vector
makes it easy to write any dimension as described above, but there may be times when you don't want to use Vector
for speed or other reasons.
3 Variable version
def Math.hypot3(x, y, z)
Math.sqrt(x * x + y * y + z * z)
end
Can be easily defined.
By the way, the reason for using x * x
instead of x ** 2
is that the former is very slow [^ square].
[^ square]: With recent square optimizations, it's likely that x ** 2
will not be too slow around Ruby 3.0.
However, the above method has 3 multiplications, 2 additions, and 1 sqrt
, and since these are all method calls, the methods are called a total of 6 times.
Such expressions may be faster if rewritten in Rust or C.
I'll write it so that even people who aren't familiar with Rust can reproduce it. However, it is assumed that Rust has already been installed.
First at the terminal
cargo new my_ffi_math --lib
And make one Rust project.
my_ffi_math is the project name.
The option --lib
specifies" create a library crate ".
A crate is a unit of compilation,
There are two types.
There is a file called Cargo.toml at the root of the project. Various settings related to the entire project are written here. At the end of this file
Cargo.toml
[lib]
crate-type = ["cdylib"]
I will add. The meaning of this is not well understood by the author.
There should be a file called src / lib.rs. The test code template is written here [^ test], but you can delete it.
[^ test]: Rust allows you to write test code in your code, and running tests is very easy, just do cargo test
.
And
src/lib.rs
#[no_mangle]
pub extern fn hypot3(x: f64, y: f64, z: f64) -> f64 {
(x * x + y * y + z * z).sqrt()
}
Write.
A keyword where fn
represents the definition of a function. pub
and ʻexternare bonuses for that, uh, I can't explain them properly. I think
pub` is something like" I'll expose this function to the outside world ".
f64
represents a type called 64-bit floating point number, which corresponds to Ruby's Float through FFI.
->
represents the return type of the function.
The contents of the function can be understood by looking at it.
Before the function definition
#[no_mangle]
I'm curious.
I'm not sure, but if I don't write this, it seems that even though I defined the function with the name hypot3
, I can't refer to it by that name after compilation.
That's all for the implementation on the Rust side.
In the project root directory
cargo build --release
If you do, it will be compiled.
build
The compiled version should be in the path target / release / libmy_ffi_math.dylib.
Oh no, the extension of the file name is Iloilo. When I did it on macOS, it became .dylib
, but it should be different on Windows [^ libext].
[^ libext]: To be precise, I think the extension of the product depends not on which OS it was compiled on, but on which target it was compiled. In other words, if you compile it for macOS (x86_64-apple-darwin) on Windows, it will be .dylib
. Rust is easy to cross-compile.
This is the only file that Ruby needs.
In this case, the base name of the file name (the part excluding the extension) is the project name with lib
at the beginning.
If you add name
to[lib]
in Cargo.toml
Cargo.toml
[lib]
name = "hoge"
crate-type = ["cdylib"]
The file name should look like libhoge.dylib
.
On the Ruby side, use the gem ffi (you can also use fiddle).
Easy with the code below
require "ffi"
But of course if you manage it with Gemfile
Gemfile
gem "ffi", "~> 1.13"
Write it in a script
require "bundler"
Bundler.require
And so on.
Also, let's assume that the Ruby code is in the root directory of the Rust project for the time being.
Write like this.
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
#reference
p Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)
# => 3.7416573867739413
The execution result is also written in the comment, but you can see that the result is the same as the one calculated in Ruby.
It doesn't matter what you set as FFIMath
, so I just prepared one module that is something nice.
ʻExtend
FFI :: Libraryfor this module. This gives rise to some singular methods such as
ffi_lib`.
The ffi_lib
method specifies the path of the compiled library file.
Well, I'm not sure, but it seems that relative paths may not work, so it seems better to give an absolute path. To do this, use File.expand_path
ffi_lib File.expand_path("target/release/libmy_ffi_math.dylib", __dir__)
And. If you write like this, the relative path from the location (__dir__
) of this file will be an absolute path.
ʻAttach_functioncreates a function created in Rust as a singular method of a module. The first argument is the function name. The second argument specifies the type of function argument. Since it has 3 arguments, it is an array of length 3.
: double` represents a FFI double precision floating point number. It corresponds to f64 in Rust and Float in Ruby.
The third argument specifies the return type of the function.
With the above, the singular method of the module can be defined.
As you can see how to use it. If you have a good idea, you may be wondering, "Hmm? You have to give Float to the argument, but you're giving an Integer?" I'm not familiar with this, but I'm sure ffi gem has converted it to floating point numbers.
It was surprisingly easy!
I was hoping for speed when I tried to implement hypot3 in Rust. Then we have to prove it by benchmark test. Well, how fast will it be!
When measuring light processing such as hypot3
, I think the benchmark test library is benchmark_driver.
In order to measure light processing, it is necessary to measure the time when the same processing is executed many times, but if you run a loop with the times
method etc., the cost of the loop cannot be ignored relatively, so it cannot be measured accurately .. Benchmark_driver runs many times without incurring such costs, so it seems that the actual execution speed can be measured (I don't know how it works).
How to use benchmark_driver
There are two ways, but this time I will try the latter. The latter requires you to remember the YAML format, but it's not that difficult.
Write as follows.
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)
For the contents of prelude
, write something that should be done before measurement.
I have defined Math.hypot3
for comparison.
If you can do this, at the terminal
benchmark-driver benchmark.yaml
And. (It is confusing that the command name is hyphen even though the gem name is underscore) Oh, install benchmark_driver gem
gem i benchmark_driver
Let's do it in advance.
Result is···
Comparison:
Math.hypot3(x, y, z): 10211285.3 i/s
FFIMath.hypot3(x, y, z): 5153872.8 i/s - 1.98x slower
eh?
Math.hypot3
can run 10 million times per second, while FFI Math.hypot3
can run 5 million times per second.
Isn't it a terrible defeat?
Far from being fast, it's too slow to talk about [^ osoi].
[^ osoi]: By the way, in this code, Integer objects are given to x
, y
, z
, but when a Float object is given, Math.hypot3
slows down by more than 10%. The FFIMath.hypot3
was unchanged.
What is the cause of the defeat? Rust's code doesn't seem to improve anymore. For compilation, I specified the release build properly. Looking at various benchmarks of Rust, it seems that it is not inferior to C, so it seems that Rust is not slow.
When it comes to that, isn't it the cost of FFI? Since Ruby can pass anything as an argument, I suspect that the ffi gem will check and convert the type at runtime. Such extra (?) Processing may be a burden.
It turned out that there seems to be no point in implementing processing of about hypot3
in Rust.
We have to do heavier processing.
Let's take a second look and think about something "heavier processing".
Recommended Posts