Rubiniusのしくみ
@highwide
表参道.rb #8
$whoami@highwide (Twitter / GitHub) (株)リブセンスでRailsを書いてます 最近は初めてのElasticsearchに四苦八苦しています
社内の「コンパイラ/インタプリタ」をテーマにしたLT大会でRubiniusについて調べたので、表参道.rbでも披露させてもらおうと今日はやってきました
よろしくお願いします!
Rubiniusって何?
Rubyの処理系の1つCRuby: CによるRubyの実装 (MRI: Matz Ruby Implementation) JRuby: JVMで動くRubyの実装 Rubinius: 作者: Evan Phoenixさん(RubyKaigi3日目Keynoteスピーカー) 高速なRuby実行環境を目指している バイトコード仮想マシンがC++で実装 LLVMによるJITコンパイルの仕組みがある コンパイラの大半がRubyで書かれている
Ruby6割 / C++3割
コンパイラの大半が Rubyで書かれている? どうやって動くの?
% cat hoge.rb 10.times do |n| puts "No. #{n}" end
% ./rubinius/bin/rbx hoge.rb No. 0 No. 1 No. 2 No. 3 No. 4 No. 5 No. 6 No. 7 No. 8 No. 9
例えば: この裏で行われていることは?
実行されるまでの流れ
Rubinius命令列
Rubyコード
トークン列
AST
LLVM命令列
機械語
バイトコード コンパイル
必要に応じて JITコンパイル
VM
実行
解釈
ちなみにMRI
YARV命令列
Rubyコード
トークン列
ASTバイトコード コンパイル
YARV
実行
解釈
バイトコードコンパイルとはRubyのコードをVMが理解できる命令列(バイトコード)にコンパイルすること ざっくり言うと パーサがRubyのコードをバラして AST(抽象構文木)を作って ASTを命令列化して 命令列オブジェクトにして プロパティを設定して VMが読める「~.rbc」形式にします
バイトコードコンパイルの各段階を 「ステージ」と呼ぶそうです
http://rubinius.com/doc/ja/bytecode-compiler/
パーサがRubyのコードをバラして (どう見てもMRIのパーサ)
ASTを作って (コンパイル時に -Aオプションで ASTを見られる)
% ./rubinius/bin/rbx compile -A hoge.rb @pre_exe: [] @name: :__script__ @file: "hoge.rb" @body: \ Send @line: 1 @name: :times @privately: false @vcall_style: false @receiver: \ FixnumLiteral @line: 1 @value: 10 @block: \ Iter @line: 1 @locals: nil @body: \ SendWithArguments @line: 2 @name: :puts @privately: true @vcall_style: false @block: nil @receiver: \ Self @line: 2 ----snip----
ちょっと余談: コンパイル時に -SオプションでS式を見られる
% ./rubinius/bin/rbx compile -S hoge.rb [:script, [:call, [:lit, 10], :times, [:arglist, [:iter, [:args, :n], [:call, nil, :puts, [:arglist, [:dstr, "No. ", [:evstr, [:lvar, :n]]]]]]]]]
% ./rubinius/bin/rbx compile -B hoge.rb ============= :__script__ ============== Arguments: 0 required, 0 post, 0 total Arity: 0 Locals: 0 Stack size: 2 Literals: 2: <compiled code>, :times Lines to IP: 1: 0..9
0000: push_int 10 0002: create_block #<Rubinius::CompiledCode __block__ file=hoge.rb> 0004: send_stack_with_block :times, 0 0007: pop 0008: push_true 0009: ret ----------------------------------------
============== :__block__ ============== Arguments: 1 required, 0 post, 1 total Arity: 1 Locals: 1: n Stack size: 5 Literals: 3: "No. ", :to_s, :puts Line: 1 Lines to IP: 2: 0..14
0000: push_self 0001: push_literal "No. " 0003: push_local 0 0005: allow_private 0006: meta_to_s :to_s 0008: string_build 2 0010: allow_private 0011: send_stack :puts, 1 0014: ret ----------------------------------------
ASTを命令列化する (コンパイル時に -Bオプションで バイトコードを 見られる)
% ./rubinius/bin/rbx compile -B hoge.rb ============= :__script__ ============== Arguments: 0 required, 0 post, 0 total Arity: 0 Locals: 0 Stack size: 2 Literals: 2: <compiled code>, :times Lines to IP: 1: 0..9
0000: push_int 10 0002: create_block #<Rubinius::CompiledCode __block__ file=hoge.rb> 0004: send_stack_with_block :times, 0 0007: pop 0008: push_true 0009: ret
def push_int(arg1) if arg1 > 2 and arg1 < 256 @stream << 4 << arg1 @current_block.add_stack(0, 1) @ip += 2 @instruction = 4 else case arg1 when -1 meta_push_neg_1 when 0 meta_push_0 when 1 meta_push_1 when 2 meta_push_2 else push_literal arg1 ----snip----
def send_stack_with_block(arg1, arg2) @stream << 57 << arg1 << arg2 @ip += 3 @current_block.add_stack(arg2+2, 1) @instruction = 57 end
これらのバイトコードは、RubyのDSLで表現されていて、仮想マシン(C++)の命令と対応している
% ./rubinius/bin/rbx compile -B hoge.rb ============= :__script__ ============== Arguments: 0 required, 0 post, 0 total Arity: 0 Locals: 0 Stack size: 2 Literals: 2: <compiled code>, :times Lines to IP: 1: 0..9
0000: push_int 10 0002: create_block #<Rubinius::CompiledCode __block__ file=hoge.rb> 0004: send_stack_with_block :times, 0 0007: pop 0008: push_true 0009: ret
send_stack~といった命令列に渡されるメソッド名はRubiniusの中で、Rubyで定義されている ※組み込みクラスはC++で書かれているので一部C++で処理するものも。
def times return to_enum(:times) { self } unless block_given?
i = 0 while i < self yield i i += 1 end self end rubinius/kernel/common/integer.rb
バイトコード※Rubiniusのビルド時に コンパイル済
言語のkernelが Rubyで書かれている ことがわかります
10.times do |n| puts "No. #{n}" end
コンパイルされた命令列を C++で書かれた
VMが評価していきます
!RBIX 1933235666460458080 22 M 1 n n x E 8 US-ASCII 10 __script__ i 10 4 10 63 0 57 1 0 21 2 17 I 2 I 0 I 0 -------snip------
hoge.rbhoge.rbc
3行 158行
ここからが Rubiniusの真骨頂 JITコンパイルだー! と思うのですが...
誤解しがちなこと (僕が誤解していたこと)
「なぜRubiniusはLLVMでコンパイルする言語なのにCやRustほど速くないの?」
"Rubinius doesn't compile Ruby down native code, instead it uses a JIT to compile certain blocks of code, but only whenever possible (or deemed necessary based on the call amounts)."
"RubiniusはRubyを機械語にコンパイルせずに、代わりに、いくつかのブロックを可能なときだけ(あるいはそのブロックの呼び出し回数に応じた必要性を判断して)、JITコンパイルを行う"
誤解しがちなこと (僕が誤解していたこと)
https://github.com/rubinius/rubinius/issues/3165
すべてのコードを JITコンパイルで
機械語にしているわけ じゃなかった
RubiniusのJITコンパイル特徴: Method JIT > メソッドが何度も呼び出されるときに行なう最適化です。この方法だと,一度コンパイルされたメソッドは再利用できますが,コンパイルには時間がかかり,インライン化によってメソッドのサイズが大きくなります。
http://gihyo.jp/news/report/01/rubykaigi2015/0003
命令1
命令1命令2
命令2命令1
命令1の定義
命令2の定義
インライン化
命令1の定義
命令1の定義
命令1の定義
命令2の定義
命令2の定義
LLVMが最適化しやすい
動的言語の性質を活かしたまま、静的な割当てが可能
おさらい
Rubinius命令列
Rubyコード
トークン列
AST
LLVM命令列
機械語
C
必要に応じて JITコンパイル
VM
実行
解釈
Ruby
C++
カーネルがRubyで 書かれているということは ActiveSupportの メソッドの移植なんて こともできるのでは?
Rubyで 書かれているので
さくっと 「try!」実装する なんてことも できました!
class Object
----snip----
def try!(*a, &b) if a.empty? && block_given? if b.arity.zero? instance_eval(&b) else yield self end else public_send(*a, &b) end end end kernel/alpha.rb
class NilClass
----snip----
def try!(*args) nil end end kernel/nil.rb
% cat try_length.rbp 'hoge'.try!(:length) p nil.try!(:length)
% ./bin/rbx try_length.rb4 nil
「kernel/」にRubyのメソッドの定義が書かれているので、Objectクラスとnilクラスにtry!を定義してコンパイル
本当はRubiniusに ぼっちオペレーター
実装しようとしたのですが parse.yが書けずに断念しました&.
Rubinius 思った以上に
おもしろかったです!
"Rubinius はただの Ruby 処理系ではない。Rubinius は Ruby コ ミュニティにとって価値ある学習リソースだ "
「Rubyのしくみ」p330
ありがとう ございました
「お詫び」または 「Thank you for your マサカリ」
Rubiniusのコードを完全に追えたわけではなく、書籍やドキュメント等の情報を元にしながらポイントごとに実装を見て、まとめました...。 おかしいなと思ったら是非ご指摘くださいm(_ _)m
謝辞社内でLTをやったあと、「Rubyのしくみ」の原文や、Rubiniusのissueから、「すべてのコードを機械語にJITコンパイルするわけではない」ということを突き止めて説明してくれた、同僚@na_o_ys氏にとっても感謝しています!!
Web https://github.com/rubinius/ http://rubinius.com/doc/ja/what-is-rubinius/- Rubinius.com 「Rubiniusとは」 https://blog.engineyard.com/2010/making-ruby-fast-the-rubinius-jit- ENGINE YARD BLOG 「Making Ruby Fast: The Rubinius JIT」 http://gihyo.jp/news/report/01/rubykaigi2015/0003 - gihyo.jp 「RubyKaigi 2015レポート Evan Phoenixさん「Rubyを速くするのは今がその時」 ~RubyKaigi 2015 基調講演 3日目」
図書 Rubyのしくみ -Ruby Under a Microscope-- Pat Shaughnessy (著), 島田 浩二 (翻訳), 角谷 信太郎 (翻訳)
参考