rust and ruby

OpenBSD upgrade

on console

sysupgrade            # finished auto reboot, same as apt full-upgrade
sysmearge             # for /etc/*.conf
pkg_add -u            # same as apt upgrade
pkg_delete -a         # same as apt autoremove
cd /usr
tar zxf ports.tar.gz
cd /usr/src
tar zxf src.tar.gz    # and sys.tar.gz
find . -name 'CVS' -exec rm -rf {} +

OpenBSDはOS部分とPKG部分が完全に分れている。だからアップグレードも2本立て。

kernelはsys、ユーザーランドはsrc、そしてpkgはportsのtar玉になってる。3ステップで、OpenBSDの全てがソースレベルでそろう。これが何といってもOpenBSDの魅力だ。

CVSって名残がちりばめられているんで、findで見付て削除しておく。sysmergeってsysmerge.shっていう600行のスクリプトなのね。思う所が有るんで、読んでみるかな。

rustc

ob$ rustc -V
rustc 1.59.0

やっと、2021年式になった。

ruby

ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [x86_64-openbsd]

タイミングが惡いとこうなる(emacs28も、タイミングが悪くて間に合わなかった)。

gdbに対処出来るようにソースを残しておこう。ならば、gdbで64bit幅のHEXを見せられるのは苦痛。そんな亊は昔から承知で32bit版のOSも用意してる。

普通にコンパイルしたら、ext/opensslの所でエラーになった。これはもう、カンニング竹山だなと思って、portsの中を調べてみたよ。

ob$ cd /usr/ports/lang/ruby/
ob$ ls
1.8/                2.3/                3.0/                patches/
1.9/                2.4/                3.1/                pkg/
2.0/                2.5/                Makefile            ruby.port.mk
2.1/                2.6/                Makefile.inc        rubygems-ext.PLIST
2.2/                2.7/                files/

これは感激、歴代のrubyを再現出来るな。で、目当てなpatchが有るかな?

ob$ ls 3.1/patches/
patch-common_mk
patch-compile_c
patch-configure
patch-ext_etc_etc_c
patch-ext_extmk_rb
patch-ext_openssl_ossl_pkey_c
patch-ext_ripper_depend
patch-include_ruby_internal_has_builtin_h
patch-lib_fileutils_rb
patch-lib_mkmf_rb
patch-lib_rubygems_commands_install_command_rb
patch-lib_rubygems_dependency_installer_rb
patch-lib_rubygems_ext_ext_conf_builder_rb
patch-template_builtin_binary_inc_tmpl
patch-thread_pthread_h

ちゃんと用意されてたので、有り難く、目でpatchを適用。他にもパッチしておいた方がよさそうだけで、testしたらall-passだったので、取り敢えずこれでいく。

tracer by debug.gem

tracerをtraceすると言う再帰の実験。題材は何でもいいんだけど、これまたtracerから頂いた例を使う。お前、何も考えていないだろう! バレたらしょうがない。素直に認めるよ。

require 'tracer'
Tracer.on
class A
  def square(a)
    return a*a
  end
end
#
a = A.new
p a.square(5)
Tracer.off
(rdbg) trace line into: LOG    # command
Enable LineTracer (enabled) into: LOG
(rdbg) c    # continue command
#0:test.rb:3::-: class A
#0:test.rb:3::C: class A
#0:test.rb:4::-:   def square(a)
#0:/usr/local/lib/ruby/gems/3.1/gems/debug-1.5.0/lib/debug/session.rb:2056:Module:>:     def method_added mid; end
#0:/usr/local/lib/ruby/gems/3.1/gems/debug-1.5.0/lib/debug/session.rb:2056:Module:<:     def method_added mid; end
#0:test.rb:7::E: end
#0:test.rb:9::-: a = A.new
#0:test.rb:10::-: p a.square(5)
#0:test.rb:4:A:>:   def square(a)
#0:test.rb:5:A:-:     return a*a
#0:test.rb:6:A:<:   end
25
#0:test.rb:11::-: Tracer.off

debug.gemをざっと見したら、独自のtracerを実装してますね。

ob$ wc LOG
   13310   79859 1288759 LOG
ob$ cat LOG | grep -v rubygems | wc
      78     467    7841

それはいいんだけど、なんでこんなに余計なものが走っているの?

irb(main):001:0> $:
=>
["/usr/local/lib/ruby/site_ruby/3.1",
 "/usr/local/lib/ruby/site_ruby/3.1/x86_64-openbsd",
 "/usr/local/lib/ruby/site_ruby",
 "/usr/local/lib/ruby/vendor_ruby/3.1",
 "/usr/local/lib/ruby/vendor_ruby/3.1/x86_64-openbsd",
 "/usr/local/lib/ruby/vendor_ruby",
 "/usr/local/lib/ruby/3.1",
 "/usr/local/lib/ruby/3.1/x86_64-openbsd"]

gemsって、あくまで vendor_ruby の一員っぽいな。

rust and ruby

Artichoke Ruby

Improving Ruby Performance with Rust

The Tie Between Ruby and Rust.

def fib(n)
  return n if n <= 1
  fib(n - 1) + fib(n - 2)
end
puts fib(38)
sakae@deb:~/artichoke$ time target/debug/artichoke fib.rb
39088169

real    1m3.490s
user    1m3.171s
sys     0m0.084s
sakae@deb:~/artichoke$ time ruby fib.rb
39088169

real    0m10.121s
user    0m10.040s
sys     0m0.028s

debugバージョンだったから、性能が出ないのかな?

sakae@deb:~/artichoke$ time target/release/artichoke fib.rb
39088169

real    0m26.839s
user    0m26.760s
sys     0m0.012s

長い時間(20分)かけてrelease版を作った結果。まだまだMRIには勝てないな。でもリリース版にしたおかげで、当初の54Mから7Mにダイエット出来たよ。後、replと言うかirb相当も有るしね。

sakae@deb:~/artichoke$ target/release/airb
artichoke 0.1.0-pre.0 (2022-04-27 revision 5606) [i686-unknown-linux-gnu]
[rustc 1.60.0 (7737e0b5c 2022-04-04) on i686-unknown-linux-gnu]
>>> 'hello'.size()
=> 5

じっくりとソースを眺めておけ。それがOSSの楽しみさ。

そして丸ごとと言う豪快な亊は勘弁と言うなら、rubyとrustの協調動作って方法がある。 rubyで時間を食てる部分をスポット的にrustにしてあげようと言う方法。ext-libを今迄はC言語で記述してたけど、それをrustで書きましょってやつね。

vbox$ cargo search ruru
ruru = "0.9.3"           # Native Ruby extensions in Rust
rutie = "0.8.4"          # The tie between Ruby and Rust.
rutie-serde = "0.3.0"    # rutie serde integration
rosy = "0.0.9"           # Ruby bindings for Rust.
magnus = "0.2.1"         # High level Ruby bindings for Rust.
rurust = "0.2.0"         # High level Ruby VM bindings

但し、ruruからrutieへと進化してるんだけど、ruby2.Xとか64Bit版とかshare-libでrubyが構築とか、linuxだけとか、制約が多い風だ。よって、オイラーは敬遠するよ。

from gem ?

Rust でつくるかんたん Ruby Gem

sakae@deb:/tmp/fib$ rake rust_build --trace
rake aborted!
LoadError: cannot load such file -- rspec/core/rake_task
<internal:/usr/local/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
<internal:/usr/local/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
/tmp/fib/Rakefile:5:in `<top (required)>'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/rake_module.rb:29:in `load'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/rake_module.rb:29:in `load_rakefile'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:710:in `raw_load_rakefile'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:104:in `block in load_rakefile'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:186:in `standard_exception_handling'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:103:in `load_rakefile'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:82:in `block in run'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:186:in `standard_exception_handling'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:80:in `run'
/usr/local/lib/ruby/gems/3.1.0/gems/rake-13.0.6/exe/rake:27:in `<top (required)>'
/usr/local/bin/rake:25:in `load'
/usr/local/bin/rake:25:in `<main>'

そんな亊知らんがな。浦島太郎だ。

VBOX$ gem install rspec
VBOX$ rake -T
rake build            # Build rutie_ruby_example-0.1.0.gem into the pkg dir...
rake build:checksum   # Generate SHA512 checksum if rutie_ruby_example-0.1....
rake clean            # Remove any temporary products
rake clobber          # Remove any generated files
rake install          # Build and install rutie_ruby_example-0.1.0.gem into...
rake install:local    # Build and install rutie_ruby_example-0.1.0.gem into...
rake release[remote]  # Create tag v0.1.0 and build and push rutie_ruby_exa...
rake spec             # Run RSpec code examples

rspecって、ruby流のテストツールだな。rakeにしろDSLだから、付き合いきれんぞ。

simple is good

rakeとかGemって苦手だ。取り敢えず、避けておこう。そこで、めちゃシンプルなやつを試す。

RubyからRustの関数をつかう → はやい

遅い所をC言語で書くんじゃなくてRust言語にしちゃえって方針。

sakae@pen:/tmp/rffi$ cat src/lib.rs
#[no_mangle]
pub extern "C" fn fib(n: u32) -> u32 {
    if n <= 1 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

こんなスペックでコンパイルする。

sakae@pen:/tmp/rffi$ cat Cargo.toml
[package]
name = "rffi"
version = "0.1.0"
edition = "2021"

[dependencies]

[lib]
name = "fib"
crate-type = ["dylib"]

そして、出来た奴をrubyに取り込むGemを用意する。 sudo gem install ffi 断トツで使われているな。裏を返せば、rubyは遅いから、何とかしたいと皆あがいているんです。

下記は、その駆動方法。

sakae@pen:/tmp/rffi$ cat rfib.rb
require "ffi"

module Fib
  extend FFI::Library
  ffi_lib 'target/release/libfib.so'
  attach_function :fib, [:uint], :uint
end

puts Fib.fib(40)

そして、実行例。

sakae@pen:/tmp/rffi$ time ruby fib.rb
102334155

real    0m12.069s
user    0m12.024s
sys     0m0.042s
sakae@pen:/tmp/rffi$ time ruby rfib.rb
102334155

real    0m0.516s
user    0m0.457s
sys     0m0.059s
sakae@pen:/tmp/rffi$ time target/release/rffi ;; only rust
102334155

real    0m0.386s
user    0m0.366s
sys     0m0.020s

ゴールデンな例だけど、この方針で十分だろう。出来上がったlibfib.soのサイズが5.1Mあるけど、見なかった亊にしておく。

fiddle

上記では Gemで供給されてるffiってのを使ってしまったけど、オイラーはGem怖い。なるべく使いたくないって亊で、標準装備の fiddle ってのも試してみた(ruby 2.7にも同梱されてる)。 library fiddle

もう、そのものって亊ですな。dlopenで目ざすライブラリィーをオープン。それから、関数名と、入力の型(複数指定出来るように配列で)、それから出力の型を指定。後は呼ぶだけーーー。

ob$ cat ffib.rb
require "fiddle"

frust = Fiddle.dlopen('target/release/libfib.so')
fib = Fiddle::Function.new(
  frust['fib'],
  [Fiddle::TYPE_INT],
  Fiddle::TYPE_INT
)

puts fib.call(40)

実は、上の奴、rustをかじった人だと、型違いでコンパイルエラーになると看破するだろう。その点rubyは緩いからね。だからRBSなんてのが開発されるんだな。Ruby 3.1.0 リリース

ちゃんとするには、BasicTypesを入れて、uintを使えるようにする。新たにモジュールを作ってから使うのが、標準な方法みたい。日本生まれのrubyは日本語が充実してて嬉しいぞと。

[sakae@fb /tmp/rffi]$ cat zfib.rb
require 'fiddle/import'
require 'fiddle/types'

module M
  extend Fiddle::Importer
  dlload 'target/release/libfib.so'
  include Fiddle::BasicTypes
  extern "uint fib(uint)"
end

puts M.fib(40)

肝は、rustに有り

ffiにしろfiddleを使うにしろ、呼出したい関数は、rustのsrc/lib.rsに記述した。 その味噌は、冒頭にある

#[no_mangle]

これのおかげで、関数名が素直に表現されている。ruby側から何の苦労もなく使える。

ob$ nm target/release/libfib.so | grep fib
000bd410 T fib
00000000 F fib.93c013d6-cgu.0
00000000 D rust_metadata_fib_5fa1543a93c013d6

このプログマを無効にして、コンパイルすると、fibって名前の前後に暗号めいた(rustで扱い易い)文字が付加されている。これじゃ、ruby側からおいそれとは、使えないだろう。

ob$ nm target/release/libfib.so | grep fib
000bd430 T _ZN3fib3fib17hca9b5a0f6149f16dE
00000000 F fib.93c013d6-cgu.0
00000000 D rust_metadata_fib_5fa1543a93c013d6

profiler

無闇やたらにsoファイルを作っても徒労に終わる。遅い所を見つけ出すのが肝要。そんな時に役立つのが、プロファイラーだ。少し探ってみる。

stackprof を、使った例。 プロファイラを使ってRubyのコードをパフォーマンス改善したい

library profile gem install profile

ちょいと使うには、これがお手軽。下記は、上で出て来たやつの実行例。

[sakae@fb /tmp/rffi]$ ruby -r profile zfib.rb
102334155
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 98.06     1.83      1.83        1  1832.80  1833.04  Fiddle::Function#call
  1.14     1.85      0.02       26     0.82     4.39  Kernel#require
  0.09     1.86      0.00        1     1.67     1.67  Fiddle::Handle#initialize
  0.06     1.86      0.00        2     0.60     2.50  Fiddle::Importer#dlload
  0.05     1.86      0.00       96     0.01     0.01  Module#method_added
  0.05     1.86      0.00        1     0.89     0.89  TracePoint#enable
  :

etc