弊社アドベントカレンダー最終日の今日がRuby2.3リリース記念日とのことで、Ruby2.3で個人的に一番待ち望んでいたバイトコードキャッシュの使い方をメモしておきます。
バイトコードキャッシュ自体は、2.3時点ではまだexperimentalな機能としてリリースしているという話ですが、例えばRuby 2.3.0-preview2では以下のようなコマンドを実行すると、一度読み込まれたソースコードのバイトコードキャッシュ(.yarbファイル)が生成されていることが確認できます。
$ ruby -v
ruby 2.3.0dev (2015-12-11) [x86_64-linux]
$ ls /usr/local/lib/ruby/gems/2.3.0/gems/activesupport-4.2.5/lib/
active_support active_support.rb
$ export RUBYOPT='-ryomikomu' && export YOMIKOMU_AUTO_COMPILE='true'
$ rails -v
Rails 4.2.5
$ ls /usr/local/lib/ruby/gems/2.3.0/gems/activesupport-4.2.5/lib/
active_support active_support.rb active_support.rb.yarb
このキャッシュを有効にするにはyomikomu
gemをrequireしたり、このgemが環境変数に依存して動作しているためいくつかの環境変数を設定することが必要になります。上記サンプルではexport RUBYOPT='-ryomikomu' && export YOMIKOMU_AUTO_COMPILE='true'
と実行してる部分です。
gemを読み込むだけでVMレベルのキャッシュが有効になるのは、このチケットでも笹田さんが記述してるのですが
The key interface is RubyVM::InstructionSequence.load_iseq(fname).
When MRI try to load any script named fname, then call this method with fname if defined.
The return value is an ISeq object, then MRI use this ISeq object instead of parsing/compiling fname file.
とのことで、gemがRubyVM::InstructionSequence.load_iseqをdefineしていて、一方でMRIはload_iseqメソッドが定義されているかどうかを判定しており、これを基準にキャッシュの有効・無効を判断しているという仕組みのようです。ソースコードとしては多分iseq.cのこの部分が該当してそうです。あと、環境変数が動作に必要なのは、単純にyomikomu
gemで環境変数を利用しているだけで、詳細はgithub.com/ko1/yomikomuで確認できます。
手元の環境で速度評価をした結果は以下の通りです。Ubuntuマシン上でbenchmark-ipsを使ってbundle version
を複数回実行して評価しました。ソース(gist)
$ ruby measure.rb
Comparison:
yomikomu(fs): 5.0 i/s
yomikomanai: 3.6 i/s - 1.40x slower
利用するコマンドにもよりますが、いくつか試した限りではbundler
を使った時の速度向上が体感しやすかったです。より詳しく考えたい方は笹田さんのrk2015での発表資料を熟読しましょう。セッションの動画も既に視聴可能な模様なので(👏)まだご覧になってない方でも冬休みで見放題だし観るしかない。
思考実験:性能改善を試みる(結果、失敗する)
Ruby3.0では3xの性能になるようですし、RubyKaigi2015でさんざん火をつけられたRubyist各位はきっと雨後の筍のようにこのバイトコードキャッシュでも性能を最適化しようと躍起になってることと存じますが、どういう改善の方向性が考えられるでしょうか。
このアドベントカレンダーで性能向上できたーって書けるとカッコいいと思ってあまり深く考えずに最初の実験として、プログラム実行中に動的にrequireするソースコードのファイル数を減らして読み込み性能を上げられないかと考えました。具体的には、通常gemの中身は複数のRubyソースコードで構成されていると思いますが、これを一回のrequire
で全て読み込めるようにRubyソースコードを結合して中間ファイルの形式にして保存しておき、2回目の実行以後はそちらの中間ファイルを読み行くようなイメージです。試した時の雑なソースコードは以下のような内容です。
module Yomikomu
class << self
attr_accessor :files_loaded
end
Yomikomu.files_loaded = {}
# gemの読み込み先のfetchはgem-pathのコードをほぼそのまま使っただけhttps://github.com/godfat/gem-path
def find_gem_path(name)
if gem_path = search_load_path(name) then
return gem_path + "/lib/#{name}.rb"
else
path = Gem.find_files("#{name}.rb").first
# favor gem first (e.g. rake gem)
if gem_path = Gem.path.find{ |p|
break $1 if path =~ %r{(#{p}(/[^/]+){2})}
}
return gem_path + "/lib/#{name}.rb"
else
return path
end
end
end
private
def search_load_path name
gem_path = Gem.path.find do |base|
gem_path = $LOAD_PATH.find do |path|
gem_path = path[%r{#{base}/(bundler/)?gems/#{name}\-[^/-]+/}]
break gem_path if gem_path
end
break gem_path if gem_path
end
gem_path[0...-1] if gem_path
end
end
include Yomikomu
def require(name)
root_name = name.slice(/(.*?)\//, 1) || name
if File.exist?("/ramdisk/#{root_name}.rb") && !ENV['YOMIKOMU_CHUKAN_CACHE'] then
super("/ramdisk/#{root_name}.rb") if Yomikomu.files_loaded[root_name].nil?
Yomikomu.files_loaded[root_name] = true
else
if ENV['YOMIKOMU_CHUKAN_CACHE'] then
found = find_gem_path(name)
if !found.nil? && File.exist?(found) && !root_name.empty? then
File.open("/ramdisk/#{root_name}.rb", 'a') { |f| f.write(File.open(found).read + "\n") }
end
end
super(name)
end
end
requireをモンキーパッチしてごちゃごちゃ処理を書いちゃってるので、処理が遅くなることは火を見るより明らかだと思うんですけど、念のため動かして、やっぱり遅くなることを確認しました(中間ファイル呼び出しによって得られる可能性のある性能改善 << requireをモンキーパッチしたことによる性能低下)。その後上記コードのうちrequire部分の処理を削りに削ってチューニングをしましたが、書き残すに値するような性能向上は認められず、このあたりでCRubyのソースコードに手を入れるなり、RubyVM::InstructionSequence.load_iseqやその下の実装を見ていく方が早そうという感触を得るに至っています。
というわけで、この年末年始に時間を見つけてCRubyのソースコードを眺めるなどして過ごしたいなという感想を持ったところが、本稿のまとめとなります。(ところで、CRubyのソースコード上でprofilingされる時ってみなさんどうされてるんですかね。。gprof等の定番ツールを使うのでしょうか?情報あれば教えてほしいです。)
なお、.yarbのバイトコードキャッシュの中身をxxd等で開いてみるとこんな感じなわけですが、構造体自体はシンプルにできてるので今のところ誰にでも読みやすいと思います。自分用メモ:compile.cのiseq_ibf_dump, ibf_dump_iseq_each, ibf_headerとかvm_core.hのrb_iseq_structあたりとかをいじって年末年始遊びたい。
$ cat hello.rb
puts 'hello world'
$ ruby -e "puts RubyVM::InstructionSequence.compile(File.open('hello.rb').read).disasm"
== disasm: #<ISeq:<compiled>@<compiled>>================================
0000 trace 1 ( 1)
0002 putself
0003 putstring "hello world"
0005 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0008 leave
$ kakidasu hello.rb
$ xxd hello.rb.yarb
0000000: 5941 5242 0200 0000 0300 0000 3a02 0000 YARB........:...
0000010: 1a00 0000 0100 0000 0200 0000 0600 0000 ................
0000020: 6901 0000 6d01 0000 2202 0000 7838 365f i...m..."...x86_
0000030: 3634 2d6c 696e 7578 002a 0000 0000 0000 64-linux.*......
0000040: 0001 0000 0000 0000 000e 0000 0000 0000 ................
0000050: 0012 0000 0000 0000 0004 0000 0000 0000 ................
0000060: 002e 0000 0000 0000 0000 0000 0000 0000 ................
0000070: 0000 0000 0000 0000 0031 0000 0000 0000 .........1......
0000080: 0000 0000 0001 0000 0001 0000 0000 0000 ................
0000090: 0014 0000 0001 0000 0000 0000 0002 0000 ................
00000a0: 0001 0000 0009 0000 0039 0000 0000 0000 .........9......
00000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000e0: 0001 0000 0000 0000 0002 0000 0000 0000 ................
00000f0: 0003 0000 0000 0000 0003 0000 0000 0000 ................
0000100: 0003 0000 0000 0000 0081 0000 0000 0000 ................
0000110: 0089 0000 0000 0000 0000 0000 0000 0000 ................
0000120: 00ff ffff ffff ffff ff00 0000 0000 0000 ................
0000130: 0000 0000 0000 0000 0089 0000 0000 0000 ................
0000140: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000150: 0000 0000 0000 0000 0001 0000 0000 0000 ................
0000160: 0001 0000 0000 0000 0099 0000 0000 0000 ................
0000170: 0000 0000 0005 0000 0000 0000 00f1 0000 ................
0000180: 0008 0000 0000 0000 0045 0000 0001 0000 .........E......
0000190: 0000 0000 0008 0000 0000 0000 0068 656c .............hel
00001a0: 6c6f 2e72 6245 0000 0001 0000 0000 0000 lo.rbE..........
00001b0: 0018 0000 0000 0000 002f 686f 6d65 2f61 ........./home/a
00001c0: 7a75 7265 7573 6572 2f68 656c 6c6f 2e72 nonymous/hello.r
00001d0: 6245 0000 0000 0000 0000 0000 0006 0000 bE..............
00001e0: 0000 0000 003c 6d61 696e 3e45 0000 0001 .....<main>E....
00001f0: 0000 0000 0000 000b 0000 0000 0000 0068 ...............h
0000200: 656c 6c6f 2077 6f72 6c64 4500 0000 0200 ello worldE.....
0000210: 0000 0000 0000 0400 0000 0000 0000 7075 ..............pu
0000220: 7473 7d01 0000 8901 0000 a501 0000 d101 ts}.............
0000230: 0000 eb01 0000 0a02 0000 5348 412d 313a ..........SHA-1:
0000240: 5041 4698 a98a 7b21 c814 41b2 bd6d 60a4 PAF...{!..A..m`.
0000250: ec10 fd4c ...L
付録:おわりに(ニジボックスの中の人向けの会社の話を少し)
ニジボックス開発室がアドベントカレンダーを外部に公開したのは今年が初めてだったわけですが(去年もmk2が中心になって社内のみでアドベントカレンダーを運用していたことはあった)、今年の枠が25枠全部埋まるとは正直思ってなかったので、なにげに胸熱です。発起人も大なり小なり手伝ったエンジニアのみんなもそれぞれ自分で自分を褒めてあげましょう。お疲れ様でした。
今年はニジボックスとしては初のPHPカンファレンスにスポンサーしたり(関連組織のMedia Technology Lab.でも大江戸Ruby会議05にスポンサー参加したり)、LT会が隔週で開催されるようになったりなど、組織的な技術活動が進捗した一年でした。来年も今年以上にスポンサー活動したり合宿行ったりなどできるように、日々の業務で汗をかきコードをかいて、顧客とユーザに価値を届けていきましょう。