34
Rubiniusのしくみ @highwide 表参道.rb #8

Rubinius Under a Microscope

Embed Size (px)

Citation preview

Page 1: Rubinius Under a Microscope

Rubiniusのしくみ

@highwide

表参道.rb #8

Page 2: Rubinius Under a Microscope

$whoami@highwide (Twitter / GitHub) (株)リブセンスでRailsを書いてます 最近は初めてのElasticsearchに四苦八苦しています

社内の「コンパイラ/インタプリタ」をテーマにしたLT大会でRubiniusについて調べたので、表参道.rbでも披露させてもらおうと今日はやってきました

よろしくお願いします!

Page 3: Rubinius Under a Microscope

Rubiniusって何?

Page 4: Rubinius Under a Microscope

Rubyの処理系の1つCRuby: CによるRubyの実装 (MRI: Matz Ruby Implementation) JRuby: JVMで動くRubyの実装 Rubinius: 作者: Evan Phoenixさん(RubyKaigi3日目Keynoteスピーカー) 高速なRuby実行環境を目指している バイトコード仮想マシンがC++で実装 LLVMによるJITコンパイルの仕組みがある コンパイラの大半がRubyで書かれている

Page 5: Rubinius Under a Microscope

Ruby6割 / C++3割

Page 6: Rubinius Under a Microscope

コンパイラの大半が Rubyで書かれている? どうやって動くの?

Page 7: Rubinius Under a Microscope

% 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

例えば: この裏で行われていることは?

Page 8: Rubinius Under a Microscope

実行されるまでの流れ

Rubinius命令列

Rubyコード

トークン列

AST

LLVM命令列

機械語

バイトコード コンパイル

必要に応じて JITコンパイル

VM

実行

解釈

Page 9: Rubinius Under a Microscope

ちなみにMRI

YARV命令列

Rubyコード

トークン列

ASTバイトコード コンパイル

YARV

実行

解釈

Page 10: Rubinius Under a Microscope

バイトコードコンパイルとはRubyのコードをVMが理解できる命令列(バイトコード)にコンパイルすること ざっくり言うと パーサがRubyのコードをバラして AST(抽象構文木)を作って ASTを命令列化して 命令列オブジェクトにして プロパティを設定して VMが読める「~.rbc」形式にします

Page 11: Rubinius Under a Microscope

バイトコードコンパイルの各段階を 「ステージ」と呼ぶそうです

http://rubinius.com/doc/ja/bytecode-compiler/

Page 12: Rubinius Under a Microscope

パーサがRubyのコードをバラして (どう見てもMRIのパーサ)

Page 13: Rubinius Under a Microscope

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

Page 14: Rubinius Under a Microscope

ちょっと余談: コンパイル時に -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]]]]]]]]]

Page 15: Rubinius Under a Microscope

% ./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オプションで バイトコードを 見られる)

Page 16: Rubinius Under a Microscope

% ./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++)の命令と対応している

Page 17: Rubinius Under a Microscope

% ./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のビルド時に コンパイル済

Page 18: Rubinius Under a Microscope

言語のkernelが Rubyで書かれている ことがわかります

Page 19: Rubinius Under a Microscope

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行

Page 20: Rubinius Under a Microscope

ここからが Rubiniusの真骨頂 JITコンパイルだー! と思うのですが...

Page 21: Rubinius Under a Microscope

誤解しがちなこと (僕が誤解していたこと)

「なぜRubiniusはLLVMでコンパイルする言語なのにCやRustほど速くないの?」

Page 22: Rubinius Under a Microscope

"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

Page 23: Rubinius Under a Microscope

すべてのコードを JITコンパイルで

機械語にしているわけ じゃなかった

Page 24: Rubinius Under a Microscope

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が最適化しやすい

動的言語の性質を活かしたまま、静的な割当てが可能

Page 25: Rubinius Under a Microscope

おさらい

Rubinius命令列

Rubyコード

トークン列

AST

LLVM命令列

機械語

C

必要に応じて JITコンパイル

VM

実行

解釈

Ruby

C++

Page 26: Rubinius Under a Microscope

カーネルがRubyで 書かれているということは ActiveSupportの メソッドの移植なんて こともできるのでは?

Page 27: Rubinius Under a Microscope

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!を定義してコンパイル

Page 28: Rubinius Under a Microscope

本当はRubiniusに ぼっちオペレーター

実装しようとしたのですが parse.yが書けずに断念しました&.

Page 29: Rubinius Under a Microscope

Rubinius 思った以上に

おもしろかったです!

Page 30: Rubinius Under a Microscope

"Rubinius はただの Ruby 処理系ではない。Rubinius は Ruby コ ミュニティにとって価値ある学習リソースだ "

「Rubyのしくみ」p330

Page 31: Rubinius Under a Microscope

ありがとう ございました

Page 32: Rubinius Under a Microscope

「お詫び」または 「Thank you for your マサカリ」

Rubiniusのコードを完全に追えたわけではなく、書籍やドキュメント等の情報を元にしながらポイントごとに実装を見て、まとめました...。 おかしいなと思ったら是非ご指摘くださいm(_ _)m

Page 33: Rubinius Under a Microscope

謝辞社内でLTをやったあと、「Rubyのしくみ」の原文や、Rubiniusのissueから、「すべてのコードを機械語にJITコンパイルするわけではない」ということを突き止めて説明してくれた、同僚@na_o_ys氏にとっても感謝しています!!

Page 34: Rubinius Under a Microscope

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 (著), 島田 浩二 (翻訳), 角谷 信太郎 (翻訳)

参考