How did yarv2llvm fail
Transcript of How did yarv2llvm fail
How Did Yarv2llvm Failyarv2llvm はどう失敗したのか
CSNagoya
三浦英樹
RubyKaigi2010
発表の流れ (Agenta)
• 速度低下の要因
• Yarv2llvm を紹介します
• Yarv2llvm を Dis ります
• ytl の紹介します
• まとめ
• ( 質問で )ytl が Dis られます
速度低下の要因(Resason of slowdown)
• Dynamic Method Search
a + b
• BOXING/UNBOXING
Fixnum#+ ?
Float#+ ?
String#+ ?
Float
1.0
a a
BOXING
a 1.0
UNBOXING
速度低下の要因(Resason of slowdown)
• Dynamic Method Search
a + b
• BOXING/UNBOXING
Fixnum#+ ?
Float#+ ?
String#+ ?
Float
1.0
a a
BOXING
a 1.0
UNBOXING
どちらも、扱っている型がコンパイル時分からないから支払うコスト
型推論で OK
速度低下の要因(Resason of slowdown)
• Dynamic Method Search
a + b
• BOXING/UNBOXING
Fixnum#+ ?
Float#+ ?
String#+ ?
Float
1.0
a a
BOXING
a 1.0
UNBOXING
どちらも、扱っている型が分からないから支払うコスト
型推論で OKでも新たな問題が・・・
yarv2llvm
• Ruby1.9 の VM(YARV と呼ばれていた )の命令列を LLVM に変換します
• Tom Bagby さんの llvmruby を使用していて、それ以外はすべて Ruby で記述してます
• 実行速度を上げるために型推論を行います
型推論の概要 ( その 1)
def f (x)
x
end
f(3)
f: α → β
型推論の概要 ( その 1)
def f (x)
x
end
f(3)
f: α → β
Fixnum
型推論の概要 ( その 1)
def f (x)
x
end
f(3)
f: α → β
Fixnum
Fixnum
Fixnum
Fixnum Fixnum
型推論の概要 ( その 2)
[1, 2, 3].map {|n| n * 2}
class Array
def map
略
res.push yield self[i]
Block : α → β
デモ
• みんな大好きフィボナッチ
• SDL が使えます
• とっても高度な Hellow World
m = LLVM::Module.new('hello') LLVM::ExecutionEngine.get(m)p_char = Type.pointer(Type::Int8Ty)ftype = Type.function(Type::Int32Ty, [p_char]) ftype = ftype.to_rawprintf = m.external_function('printf', ftype) ftype = Type.function(Type::Int32Ty, []) ftype = ftype.to_rawmain = m.get_or_insert_function('main', ftype) b = main.create_block.builderstrptr = b.create_global_string_ptr("Hello World! \n")b.call(printf, strptr)b.return(strptr)LLVM::ExecutionEngine.run_function(main)
Yarv2llvm を Dis ります
• おもったより速くないんだよね
• 出来そうで、出来ない。すこしバグってる型推論
• Ruby フルセット? 美味しいのそれ?
0
1
2
3
4
5
6
7
8
9
10
11
12
Rate Ruby1.9 / Yarv2llvm
なぜ遅いのか
• ランタイム ( 文字列処理や GC) は Ruby1.9 と同じ処理
• BOXING が必要– Ruby1.9 のランタイムを呼ぶために→ GC ができない
– 中には余分な BOXING で Ruby1.9 より遅くなる場合も
– 解決策はランタイムを GC を含めて作りなおすこと
Ruby そのものでランタイムを書くのが良い
ランタイムを Ruby で書く場合
あらかじめコンパイルしておいてリンクすればよいというわけではない
型推論の結果による Specializationが前提– 例えば
[1, 7, 2, 9].sort → 比較処理を整数型限定にすることで BOXING を避ける
コンパイル時間の増大
型推論出来ないプログラム
• それは bm_app_pentomino.rb から始まった
def bar(arr)
arr.map {|n| n * 2}
end
def foo(arr)
bar(arr.map {|n| n * 2})
end
p foo(["1", "2", "3"])
型推論できないプログラム
• それは bm_app_pentomino.rb から始まった
この型が決まらないと
def bar(arr)
arr.map {|n| n * 2}
end
def foo(arr)
bar(arr.map {|n| n * 2})
end
p foo(["1", "2", "3"])
型推論出来ないプログラム
• それは bm_app_pentomino.rb から始まった
このメソッドが何なのか決定できない
def bar(arr)
arr.map {|n| n * 2}
end
def foo(arr)
bar(arr.map {|n| n * 2})
end
p foo(["1", "2", "3"])
型推論出来ないプログラム
• このバグからいろいろ崩壊
– 型推論が 1 ステップでは出来ず、型推論の実行時間が増大する
• All pure ruby では荷が重くなってきた
– そもそもプログラムのメンテナンス性が悪すぎてこのバグが解決できなかった
• 直そうとすると別のところが動かなくなる
• LLVM コード生成と型推論が混然一体となっている
• クロージャーの乱用
Yarv2llvm で実装出来ない機能
• Send• 引数が定数ではない require( 動的 require)• Eval• 動的再定義
• Bignumいわゆる、黒魔術 (Bignum は除く )そもそも、実現する必要があるのか?
➭ こういうのを実現しないと Ruby の処理系と認めてくれない
Yarv2llvm で実装出来ない機能
黒魔術は何が問題か →
型推論の結果をくつがえしてしまう
想定しない型のデータがやってきてバグる
例
a = 10
eval(“a = ‘foo’”) print (a * 10) # Fixnum の掛け算命令を生成すると
?
Yarv2llvm で実装出来ない機能
ではどうするのか?
新しい型情報で型推論とコンパイルをし直す
Compile and type inference with new type inforamation
コードだけではなく、スタックフレームやオブジェクトも新しい型情報に合わせて書き換える必要がある場合もある
on-stack replacement
実現の難しさ
Easy Send ↑ 動的メソッド定義、動的 require
↓ Eavl( 型変更なし ) Hard Bignum , Eval( 型変更あり )
Bignum が最も難しい
・ 実行中のメソッドも書き換える必要がある
• スタックフレーム中のデータも書き換える必要がある
• 比較的利用頻度が多いので遅くするわけにはいかない
a = 1
eval(“a = 1.3”)
c = a + 3
Ytl の紹介
• フルセットを目標に開発しているトランスレータ
• 独自のネイティブコード生成ライブラリ(ytljit) を構築– フルセット Ruby+ 型推論をサポートするため、個性的な ( 癖のある ) 仕様
– アセンブラベース
– X86 ネイティブコード (32/64bit) を直接生成• マクロ・疑似命令が ABI を含めて 32/64bit の違いを吸収
– LLVM はもう使わない
言い訳
• Ytl は未完成です– 簡単な Ruby プログラムが型推論 * 無し * でコンパイル出来る程度です。
• Ytl は yarv2llvm より ** 遅い ** です– 数値計算を多用するプログラムの場合
– LLVM は化け物です。あの最適化とは勝負できません
– LLVM 相当の最適化が可能になった時 yarv2llvmより確実に速くなるでしょう
Structure of ytl
VM
Translator
Type inference M echanism
YARV C ode
Assem bler
C ode Space
Type inference Policy
ytljit
M arshal for VM and C odeSpace
Code Space• 実行可能なネイティブコードを格納する
• コード書き換えを前提としたデータ構造
• コード書き換え後も参照関係を自動的に維持する
add eax, 1
jz Foo
mov eax, 0
Foo
add eax, 1
add eax, 1
jz Foo
mov eax, 0
jmp Foo
Foo
sub eax, 1
コードを 1 つの領域にまとめる
書き換えると全部書き換えないといけない
基本ブロック毎に分割
参照関係を自動的に更新
Foo
check bignum
call _bignum_sub
realloc自動的に更新
VM• Ruby プログラムの中間表現
– コンパイル時だけではなく、実行時も保持
– ネイティブコードを生成して実行
• 型推論を行うことができる– ネイティブコード生成時には型推論を行う
– 再コンパイル時には必要な制約をつけて型推論をやり直すことができる
– 型推論は ytl で SelfCompile したネイティブコードで実行
• 構造はシーケンスではなく、グラフ構造– 型推論をやり易くするため
• Marshal 可能 ( 型推論ルーチンも含めて )
実行の流れ
YARV
VM( 型無し )
VM( 型付 )
ASM
Native Code
putobject 1
putobject 1
send :+
Lit: 1 Lit: 1
Send :+
Lit: 1: Fixnum Lit: 1:Fixnum
Send :fixnum_+ : Fixnum
mov eax, 1
add eax, 1
0101110001111
変換
型推論
コンパイル
アセンブル
再コンパイル
Yarv2llvm での課題の解決策( その1)
• ランタイムを Ruby で書くことによるコンパイル時間の増大– VM を Marshal 可能にする
– VM レベルで型推論を行うので、型推論によるSpecialization は可能
• 型推論の複雑化と推論時間の増大– コード生成と型推論を分ける
– 型推論処理を Self Compile してネイティブコードで実行する
Yarv2llvm での課題の解決策( その 2 )
• Bignum をはじめとする実装できない機能– これらを実装するため動的再コンパイルを採用
→ デモの Hello World はこのための実験だった
– 実行時のスタックフレームの構造を完全に把握するためコード生成はアセンブラレベルで行う
→ LLVM ではバージョン・オプティマイズによりスタックフレームの構造が変わる可能性大
– 効率よく動的再コンパイルを実現するため、 Code Space に工夫
まとめ
• Yarv2llvm では主に3つ問題がある– 速度 (CRuby ランタイム使用時の BOXING)– 不完全で遅い型推論
– eval, bignum 等実現できない仕様がある
• 問題を解決するために ytl を開発中である– ランタイムを Ruby で記述できるように Marshal 可能にする
– 型推論の Native Code 化– On-Stack Replacement とそれを支援する仕組み
ご清聴ありがとうございます
BigNum の実現方法 a = a + b
overflow
対応する VM のノードを調べる
a の型を bignum に変更
型推論、コンパイルのやり直し
スタックフレームの書き換え
restart にジャンプ ( 書き換え後のコードであることに注意 )
add a, b
jo overflow
restart:
:
型推論の概要 ( その 3)
[1, “2”, 3].map {|n| n * 2}
class Array
def map
略
res.push yield self[i]
Block : α → β
Fixnum or String
Generate Dynamic Dispatch
自己紹介
• Ruby と Lisp(! 声優 ) の好きな水道屋です– S式入力の下水道図面生成プログラムとか
• Blog (miura1729 の日記 ) http://d.hatena.ne.jp/miura1729
• Twitter @miura1729
• http://github.com/miura1729