cookpad 17 day tech internship 2017 言語処理系入門 rubyをコンパイルしよう

85
Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう 笹田 耕一 クックパッド株式会社 [email protected]

Upload: koichi-sasada

Post on 23-Jan-2018

11.551 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Cookpad 17 day Tech internship 2017

言語処理系入門Rubyをコンパイルしよう

笹田耕一クックパッド株式会社

[email protected]

Page 2: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

今日の講義

•Ruby のコンパイラ開発を通じて言語処理系を作ってみよう。•基礎:構文解析結果 → バイトコードへの変換•発展:さらなる最適化・高速化

Page 3: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

自己紹介:笹田耕一http://atdot.net/~ko1/

•所属:クックパッド株式会社•2006-2012 大学教員•2012-2017 Heroku, Inc.•2017- Cookpad Inc.

•仕事:Rubyインタプリタの開発•コア部分の開発•VM、スレッド、GC、その他•Ruby を使う仕事ではない

3

Page 4: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Ruby interpreter

Ruby (Rails) app

RubyGems/Bundler

So many gemssuch as Rails, pry, thin, … and so on.

普通のRubyプログラミングi gigantum umeris insidentesStanding on the shoulders of giants

Page 5: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Interpret on RubyVM

ふつうの Ruby 処理系プログラミング

5

Rubyscript

Parse

Compile(codegen)

RubyBytecode

Object managementGarbage collectorThreading

Embeddedclasses and methods

BundledLibraries

Evaluator

GemLibraries

ASTAbstract Syntax Tree

Page 6: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Ruby 処理系概要

Page 7: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Ruby処理系の構成概要

7

•プログラムを読んで•実行

Page 8: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Interpret on RubyVM

Ruby 処理系の流れ

8

Rubyscript

Parse

Compile(codegen)

RubyBytecode

Object managementGarbage collectorThreading

Embeddedclasses and methods

BundledLibraries

Evaluator

GemLibraries

ASTAbstract Syntax Tree

読むところ 実行するところ

Page 9: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Interpret on RubyVM

Ruby 処理系の流れRuby 1.8 以前

9

Rubyscript

Parse

Object managementGarbage collectorThreading

Embeddedclasses and methods

BundledLibraries

Evaluator

GemLibraries

AST

Page 10: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ASTとRuby 1.8

10

a =

MethodDispatch(:+)

cb

Abstract Syntax TreeRuby Program

a = b + c

a =

MethodDispatch(:+)

cb

a =

MethodDispatch(:+)

cbRuby 1.8 はAST を単純にたどるインタプリ

Parse

Page 11: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Interpret on RubyVM

Ruby 処理系の流れRuby 1.9 以降

11

Rubyscript

Parse

CompileRuby

Bytecode

Object managementGarbage collectorThreading

Embeddedclasses and methods

BundledLibraries

Evaluator

GemLibraries

AST

Page 12: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

VM – Stack Machine

12

Ruby Program

a = b + c

getlocal bgetlocal csend :+, 1setlocal a

YARV Instructions

a

b

c b

c

b+c

b+c

YARV Stack

Compile

今日は、スタックマシンをたくさん使うよ。

Page 13: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

余談 スタックマシン vs レジスタマシン

•レジスタマシン•計算対象・格納場所にレジスタを指定•物理CPUはもっぱらレジスタマシン

•スタックマシン•計算対象・格納場所は(暗黙に)スタック•レジスタ指定がない分スリム•命令数は多くなることがある•言語VMでは多い(最近はレジスタマシンも)

Page 14: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

今日の課題

•既存のコンパイラ(C で実装)の代わりに、Ruby で Ruby のコンパイラを書こう。

課題A. ヒューマンコンパイラ

課題B. 自動コンパイラ

Page 15: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Interpret on RubyVM

Ruby 処理系の流れ

15

Rubyscript

Parse

Compile(codegen)

RubyBytecode

Object managementGarbage collectorThreading

Embeddedclasses and methods

BundledLibraries

Evaluator

GemLibraries

AST

今日最終的に作るもの

Page 16: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

入力と出力Rubyscript

Parse

Compile(codegen)

RubyBytecode

ASTAbstract Syntax Tree

今日つくるもの

これ全部でコンパイラということも。

Page 17: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

入力と出力ほかの言語では?

X languageprogram

Parse

Compile

Code

ASTAbstract Syntax Tree

入力 出力

C コンパイラ(gccとか) C プログラム 機械語(アセンブラ)

Java (javac) Java プログラム JavaVM バイトコード(.class)

JavaScript (babel) JavaScript (ES6, …) JavaScript (ES5)

Ruby Interpreter Ruby プログラム Ruby VM バイトコード

Ruby InterpreterJIT compiler

Ruby プログラム C ソースコード(実行時にコンパイル&ロード)

デモ:実際に Ruby VM バイトコードを見てみよう

Page 18: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

AST: Abstract Syntax Tree# Ruby script

a = 10if a > 1

p :okelse

p :ngend

Program

LvarAssign if

a10

Lvar

send

a

>

1

send(fcall)

p

:ok

send(fcall)

p

:ng

字句解析構文解析

Tips: 字句解析・構文解析について、詳しくは去年の青木さんの資料を読もうhttps://speakerdeck.com/aamine/cookpad-2016-summer-intern-programming-paradigm

Seq

Literal

Literal Literal Literal

Page 19: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Ruby Bytecode

# Ruby scripta = 10if a > 1

p :okelse

p :ngend

0000 putobject 10

0002 setlocal a, 0

0005 getlocal a, 0

0008 putobject 1

0010 send <callinfo!mid:>, argc:1, ARGS_SIMPLE>,

<callcache>, nil

0014 branchunless 27

0016 jump 18

0018 putself

0019 putobject :ok

0021 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0025 jump 34

0027 putself

0028 putobject :ng

0030 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0034 leave

Page 20: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

番地 スタックの状況(空でスタート) 解説0000 putobject 10 # [10] スタックに 10 をプッシュ0002 setlocal a, 0 # [] a にスタックトップの 10 をセット0005 getlocal a, 0 # [10] a の値をスタックにプッシュ0008 putobject 1 # [10, 1] 1 をスタックにプッシュ0010 send <callinfo!mid:>, argc:1, ARGS_SIMPLE>,

<callcache>, nil 10.>(1) というメソッド呼び出し# [true] その結果(true)をプッシュ

0014 branchunless 27 # [] スタックトップの値が false or nil なら 27 へ0016 jump 18 # [] 無条件に 18 へジャンプ0018 putself # [self] self をスタックへプッシュ0019 putobject :ok # [self, :ok] :ok をスタックへプッシュ0021 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil p(:ok) を実行# [:ok] 結果の :ok をスタックにプッシュ

0025 jump 34 # [:ok] 無条件に 34 へジャンプ0027 putself

0028 putobject :ng

0030 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0034 leave # [] このコードを終了する(積んであった :ok を返す)

スタックマシンの実行を解説

すべての計算が、「スタックから値を取り出し」、「スタックに積む」ことで実現されていることがわかればOK

Page 21: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

どんな命令があるの?

•Ruby のソースの insns.def に書いてある•https://github.com/ruby/ruby/blob/trunk/insns.def

•opt_ と付いている命令は見る必要はない•Optimization(最適化)のための命令群

Page 22: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

例:getlocal

/**

@c variable

@e Get local variable (pointed by `idx' and `level').

'level' indicates the nesting depth from the current block.

@j level, idx で指定されたローカル変数の値をスタックに置く。level はブロックのネストレベルで、何段上かを示す。

*/

DEFINE_INSN

getlocal # ← 命令の名前(lindex_t idx, rb_num_t level) # 命令オペランド() # スタックからとってくる値(今回はなし)(VALUE val) # 終了時、スタックへプッシュする値{ # C での実装

val = *(vm_get_ep(GET_EP(), level) - idx);

RB_DEBUG_COUNTER_INC(lvar_get);

(void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0);

}

https://github.com/ruby/ruby/blob/trunk/insns.def#L47

Page 23: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

入力と出力

CompileRuby

BytecodeAST

Page 24: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

今日やること

0000 putobject 10

0002 setlocal a, 0

0005 getlocal a, 0

0008 putobject 1

0010 send <callinfo!mid:>, argc:1, ARGS_SIMPLE>,

<callcache>, nil

0014 branchunless 27

0016 jump 18

0018 putself

0019 putobject :ok

0021 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0025 jump 34

0027 putself

0028 putobject :ng

0030 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0034 leave

別の言い方をすると木構造から列構造への変換

RubyBytecode

ASTProgram

LvarAssign if

a10

Lvar

send

a

>

1

send(fcall)

p

:ok

send(fcall)

p

:ng

Seq

Literal

Literal Literal Literal

Page 25: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題Aヒューマンコンパイラ

Page 26: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

こんな言葉を聞いたことはないですか?

•「コンパイラの気持ちになって考えよう」

•類例•「CPUの歓声が聞こえる」•「OSになったつもりで管理する」•「パケットの気持ちになって考える」•「AWSの気持ちになる」

Page 27: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A ヒューマンコンパイラ

•「コンパイラの気持ちになって考えよう」

•Ruby プログラムを見て、VM のバイトコードを人力で生成しよう。

•(別の言い方をすると)Ruby 言語をスタック型言語(VMアセンブラ)書き直してみよう。

Page 28: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

「おまえがコンパイラになるんだよ!」

Rubyscript

RubyBytecode

課題A全体構成

Page 29: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ツールRubyVM::InstructionSequence

•組み込みクラス RubyVM::InstructionSequence•略して ISeq•バイトコードを扱うためのクラス

# example

ISeq = RubyVM::InstructionSequence # 長いのでiseq = ISeq.compile(script) #=> ISeq を生成p iseq.eval #=> iseq を実行(結果表示)puts iseq.disasm #=> 逆アセンブル結果を表示

Page 30: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ツールISeq Array 相互変換

•今回は ISeq Array 相互変換の仕組みを利用• ary = iseq.to_a #=> 配列を生成• ISeq.load(ary) #=> ISeq• VM命令は、この配列によって生成可能• …しかし、いちいち配列を作るのは面倒くさい

pp ISeq.compile(‘’).to_a # 何もないプログラムの場合["YARVInstructionSequence/SimpleDataFormat",2, 3, 1,{:arg_size=>0, :local_size=>0, :stack_max=>1},"<compiled>", "<compiled>",nil, 1, :top, [], {}, [],[[:putnil], [:leave]]] # バイトコード部分はここだけ

Page 31: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ツールYASM: YARV Assember

•yasm.rb•Ruby VM 用アセンブラ•命令を素直に Ruby で書けば、ISeq を生成可能

Page 32: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ツールYASM: ISeqの配列を便利に作る仕組み

# YASM example

iseq = YASM.asm label: ‘integer:1' do# ブロックに命令を書くことで ISeq 生成putobject 1

leave

end

p iseq.eval #=> 1

Page 33: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A「ヒューマンコンパイラ」の進め方

•課題ファイル asm/task.rb•テストケースになっている。•どんどんアセンブラを埋めていこう。

•調査のための方法• yasm.rb の最後に直接記述

•最後の “# fill your asm here” の箇所にアセンブラ記述• “$ ruby yasm.rb” で ISeq と実行結果を表示

• try.rb の script 変数に調べたい Ruby プログラム文字列を入力し、実行すると、正解データ(逆アセンブラ)が出てくるので参考にしよう。•ほかの人・TA・講師にどんどん聞こう。

Page 34: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方起動

•asm/task.rb に解答を書いた場合•$ ruby asm/task.rb•答えが想定と異なればエラー出力し実行終了

•yasm.rb に直接書いた場合•$ ruby yasm.rb•これで実験できる

Page 35: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方基本

•プログラムの終わり方• leave 命令: プログラムの最後は必ず leave •スタックトップを返値として返す•つまり、スタックトップに値が一つ(だけ)必要

•スタック操作• putobject X # スタックに X(整数や true/false)を積む• putnil # nil を積む• putstring “xxx” # スタックに文字列 “xxx” を積む• pop # スタックから一要素取り外す• dup # スタックの一番上と同じものをもう一つ積む

Page 36: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-1 整数値

•整数値を返すプログラム•1•1_000_000

•Hint: putobject を利用

Page 37: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-1’ シンボル

•シンボルを返すプログラムを変換• :ok• :ng

•Hint: putobject を利用

Page 38: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-1’’ 文字列

•文字列を返すプログラムを変換• “hello”

•Hint: putstring を利用

Page 39: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方ローカル変数アクセス

•命令• getlocal :lvar # 変数 lvar の値をスタックトップに• setlocal :lvar # 変数 lvar の値をセット• :lvar のように変数名をシンボルで指定

•逆アセンブラ表記とちょっと違うので注意• getlocal lvar, 0 のように出てくる。• yasm では 0 を省略可能(書いても良い)。

Page 40: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-2 ローカル変数

•ローカル変数のset, get を含むプログラムを変換• “a = 1; a”(設定して、取得している)

•Hint: putobject, getlocal, setlocal を利用

Page 41: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-3 self, nil

•“self” を返すプログラムを実装•ローカル変数と合わせると、“a = self” が動く

•“nil” というプログラムを実装

•Hint: putself, putnil を利用

Page 42: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド呼び出しRuby のメソッド呼び出し再入門

• receiver.method_name(p1, …, pn)•レシーバ(receiver)のある普通のメソッド呼び出し• method_name: メソッドの名前• p1, …, pn: n 個の引数

•method_name(p1, …, pn)•レシーバが省略されたメソッド呼び出し• self が省略されている• private メソッドを呼ぶことができる(pなど)

Page 43: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド呼び出しRuby のメソッド呼び出し再入門

•binary operator(二項演算子)•1+2 は 1.+(2) というメソッド呼び出し

• receiver: 1•メソッド名: +(Symbol では “:+”)•引数は 2 (1個だけ)

Page 44: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド呼び出しsend 命令を利用

# 逆アセンブラの表記(1+2 => 1.+(2) の逆アセンブラ)0000 putobject 1 # レシーバを積む0002 putobject 2 # 1番目の引数を積む0004 send <callinfo!mid:+, argc:1, ARGS_SIMPLE>, <callcache>, nil

0008 leave

# アセンブラでの表記putobject 1

putobject 2

send :+, 1 # 簡単!(ARGS_SIMPLE は無視)leave

よくわからない…

Page 45: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド呼び出しsend 命令の書き方

• send method_id, argc, flag(ないなら省略可能)

•method_id はメソッド名のシンボル(:p など)

•argc は引数の数

• flag はメソッド呼び出しの種類(省略可能)• 1+2 は 1.+(2) なので send :+, 1(flagなし)• foo() のように self が無い→ YASM::FCALL

• p(1) は、send :p, 1, YASM::FCALL

•ほかのフラグは無視してよい

Page 46: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド呼び出しsend 命令の使い方

•スタック上に、receiver と引数を積み send• receiver.mid(引数1, …, 引数n) の場合

• receiver, 引数1, 引数2, …, 引数nを積む命令群

• send :mid, n

•実行後、receiver.mid(引数1, …, 引数n) の結果だけが積まれる

• receiver がない(mid(…) の場合)も、receiver として self を積んでおく(YASM::FCALL を指定)

Page 47: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

receiver

param1

param2

paramN

result

send :mid, N

YASMの使い方:メソッド呼び出しsend 命令の使い方(VMスタック)

receiver.mid(param1, …, paramN) の場合

Page 48: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

self

param1

param2

paramN

result

send :mid, N, YASM::FCALL

YASMの使い方:メソッド呼び出しsend 命令の使い方(VMスタック)

mid(param1, …, paramN) の場合

Page 49: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

self

:ok

result (:ok)

send :p, 1, YASM::FCALL

YASMの使い方:メソッド呼び出しsend 命令の使い方(VMスタック)

p(:ok) の場合(receiver として self を積む)

Page 50: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

1

2

3

send :*, 1

YASMの使い方:メソッド呼び出しsend 命令の使い方(VMスタック)

1 + 2 * 3 の場合

1

result(6)

send :+, 1

result(7)

1

1 + ??

2 * 3

1 + 6

Page 51: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-4 メソッド呼び出し

•receiver のあるプログラムを変換• “1 < 10” つまり “1.<(10)” というプログラム

•receiver のないプログラムを変換• “p(1)” というプログラム•Hint: receiver の putself を忘れずに

Page 52: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-4’ メソッド呼び出し(組み合わせ)

•組み合わせる(いろんなことが出来る)• “1 - 2 * 3” #=> -5• “a = 1; b = 2; c = 3; a - b * c” #=> -5• “a = 10; p(a > 1)” #=> true• “p(‘foo’.upcase)” #=> ‘FOO’

Page 53: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:ジャンプ命令とラベル

•逆アセンブラでは、ジャンプ先はアドレス→ アドレス計算は面倒なので、ラベルで指定

•ジャンプ命令• jump :label_name• branchif :label_name• branchunless :label_name

•ラベル• label :label_name

# examplelabel :begin

putselfputobject 1send :p, 1, YASM::FCALLjump :begin # begin ラベルへジャンプ

# つまり無限ループ

Page 54: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-5 if 文

•例題として出したプログラムを変換

•Hint: label と branchif or branchunless を上手に使う

# Ruby scripta = 10if a > 1p :ok

elsep :ng

end

Page 55: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-5’ else の無い if 文は?

•else の無い次のプログラムを変換

•Hint: 実際に実行して、if文の値を確かめよう

# Ruby script (1)a = 10if a > 1p :ok

end

# Ruby script (2)a = 10if a < 1p :ok

end

Page 56: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-6 while 文

•ヒント:jump と branch* で while を表現。•ヒント:pop 命令を(多分)利用します。

a = 0while(a < 10)p aa += 1 #=> a = a.+(1)

enda #=> 10

Page 57: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド定義

• “core#define_method” という特殊なメソッドを利用•実は、Ruby のメソッド定義は特殊なメソッド呼び出しによって行われている

• Rubyだと: SpecialObject. core#define_method(:mid, iseq)

• ISeq を生成し、この “core#define_method” を呼ぶ

•… が、面倒くさい # プログラム例# SpecialObject. core#define_method(:mid, iseq)

m_iseq = YASM.asm(…) do

end

putspecialobject 1 # レシーバ:SpecialObject を取り出すputobject :mid # 第一引数:メソッド名putiseq m_iseq.to_a # 第二引数:メソッドの実体send :“core#define_method”, 2 # 呼び出し

# 特殊なメソッド名であることに注意

Page 58: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド定義define_method_macro

•メソッドボディの ISeq の生成+メソッド定義命令列を生成するメソッド

# define_method_macro の利用例# def foo(a); p(a); end を行う命令列を生成define_method_macro :foo, parameters: [:a] do

putself

getlocal :a

send :p, 1, YASM::FCALL

leave

end

# ここにメソッドを定義する命令列が挿入されるpop # 定義後、method 名のシンボルがスタックに積まれているので、pop

putself

putobject 123

send :foo, 1, YASM::FCALL # foo(123)

Page 59: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

YASMの使い方:メソッド定義define_method_macro

•define_method_macro を使った結果を確認

iseq = YASM.asm do

define_method_macro :foo, parameters: [:a] do

putself

getlocal :a

send :p, 1, YASM::FCALL

leave

end

leave

end

puts iseq.disasm

ruby 2.5.0dev (2017-08-04 trunk 59496) [x64-mswin64_140]

== disasm: #<ISeq:<compiled/yasm(top)>@yasm.rb:181:in `<main>'>=========

0000 putspecialobject 1 …

0002 putobject :foo

0004 putiseq foo

0006 send <callinfo!mid:core#define_method, argc:2>, …

0010 leave

== disasm: #<ISeq:[email protected]:79:in `define_method_macro'>==============

local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, …

[ 1] a<Arg>

0000 putself ( 1)

0001 getlocal a, 0

0004 send <callinfo!mid:p, argc:1, FCALL>, <callcache>, nil

0008 leave

Page 60: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-7 メソッド定義

def fib(n)if n < 2

1else

fib(n – 2) + fib(n-1)end

endfib(10)

# paramdef foo(a)

aendfoo(100)

# no paramdef foo()end

引数+呼び出し fib 定義と fib(10) の呼び出し

Page 61: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

高速化・最適化

•命令の置き換えで高速化ができることも。

•本当にたくさんの方法があるので、興味がある人がいれば調べてみよう&fibを高速化してみよう。

書籍:コンパイラの構成と最適化(中田育男)

Wikipedia: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9%E6%9C%80%E9%81%A9%E5%8C%96

Page 62: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

簡単な最適化の例ピープホール(のぞき穴)最適化# 例a = 1

if a > 10

1

else

2

end

# no-opt

putobject 1

setlocal :a, 0

getlocal :a, 0

putojebt 10

send :>, 1

branchunless :else

jump :body

label :body

putobject 1

jump :end

label :else

putobject 2

label :end

leave

# w/ peephole opt

putobject 1

setlocal :a, 0

getlocal :a, 0

putojebt 10

send :>, 1

branchunless :else

putobject 1

leave

label :else

putobject 2

leave

Page 63: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題A-8

•Rubyでコンパイルした fib() と、自分でアセンブルした fib() の速度比較をしよう。• asm/asmfib.rb に task.rb から fib 定義部分をコピペ•実行して比較(それぞれ fib(35) 実行時間を計測)

• $ time ruby asm/asmfib.rb 35• $ time ruby asm/fib.rb 35

•ついでに、asm/fastfib.rb も調べてみよう(fib(10_000) など呼んでみよう)

Page 64: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

余談:最適化とは?

•何が最適であるか?•実行時間?•メモリサイズ?•プログラムを書く時間?•ほかには?

•プログラムの意味とは?•プログラマの意図とは?•「最適を導く」言語設計とは?

Page 65: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

余談:DSLDomain Specific Language

•「問題を解くには、まずは言語を作る」

•DSL: Domain Specific Language•外部DSL•内部DSL

•YASM は内部DSL•C のアセンブラを見たことがありますか?•それに対して、YASM はどうでしたか?

Page 66: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

発展課題A

•もっといろんなRubyプログラムを変換•例えば

• (インスタンス|グローバル|クラス)変数•定数•ブロックに対応•例外処理に対応

•取り組み方• ISeq#dump, #to_a の結果をじっと見る• Ruby のソースコード(C)をじっと見る• yasm.rb を変更・拡張する

Page 67: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題B自動コンパイラ

Page 68: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

人間がコンパイルするのはつらい

•人間は単純作業が苦手。

•人間はミスをする。

→ コンピューターにやらせよう。

Page 69: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題B: AST (node tree) → Bytecode (ISeq)

0000 putobject 10

0002 setlocal a, 0

0005 getlocal a, 0

0008 putobject 1

0010 send <callinfo!mid:>, argc:1, ARGS_SIMPLE>,

<callcache>, nil

0014 branchunless 27

0016 jump 18

0018 putself

0019 putobject :ok

0021 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0025 jump 34

0027 putself

0028 putobject :ng

0030 send <callinfo!mid:p, argc:1, FCALL|ARGS_SIMPLE>,

<callcache>, nil

0034 leave

RubyBytecode

ASTProgram

LvarAssign if

a10

Lvar

send

a

>

1

send(fcall)

p

:ok

send(fcall)

p

:ng

Seq

Literal

Literal Literal Literal

Page 70: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題Bの進め方

•ast2iseq/ast2iseq.rb のast2iseq(ast)を完成させよう。• ast2iseq_visitor.rb の中身を ast2iseq.rb にコピペ。

• visitor pattern によるast2iseq() のスケルトンがある。•参考までに全部で 3 パターン用意

• ast2iseq_visitor.rb: visitor pattern を利用

• ast2iseq_func.rb: 再帰関数で case/when を利用

• ast2iseq_composite.rb: composite pattern を利用

• 違いを考察するのも面白いかも?

•答えも用意(ast2iseq_ans_....rb)•あまり見ないでね。

Page 71: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題Bの進め方(試行錯誤)

1. ast2iseq メソッドを(途中まででも)作る

2. ファイル下部にある script 変数にコンパイル対象の Ruby スクリプトを入力(例: 1+2)

3. “$ ruby ast2iseq/ast2iseq.rb” を実行1. Ruby2AST.to_ast(script)で、Ruby を AST に変換(ついでに見やすく

出力)

2. ast2iseq(AST) を実行a. 未完成なのでエラー

b. 実装済みなので正しい答えを得る

4. 3 の結果を見て、じっくり考えて 1 に戻る(迷ったら try.rbで本家Rubyのコンパイル結果の逆アセンブラを見る)

Page 72: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

課題Bの進め方(テスト)

• iseq2ast/test.rb を通す•このファイルを実行すると、開発した ast2iseq を使い、課題A の各コードをコンパイル、実行し、正しい答えであるかをチェックする。•最終的に全部テストが通ったらOK

Page 73: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ヒント:YASM の使い方

•YASM::asm の代わりに YASM.new を利用

# example

yasm = YASM.new(label: …) # YASM.asm と同じ引数yasm.putnil #

yasm.leave # 2命令生成iseq = yasm.to_iseq # 2命令分の ISeq を生成puts iseq.disasm # 逆アセンブルiseq.eval # 実行

Page 74: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ヒント:Node の構成• Ruby2AST.to_ast(script) の結果を見て、ノードのデータ構造をチェック。• ast2iseq/ruby_nodes.rb に定義。

• ProgramNode• SequenceNode• NilNode• SelfNode• LiteralNode• StringLiteralNode• LvarAssignNode• LvarNode• SendNode• IfNode• WhileNode• DefNode

• 図にしてみるとわかりやすい?

# “1 + 2” というプログラムのノード#<ProgramNode

@seq_node => #<SequenceNode

@nodes => [#<SendNode

@type => :call@receiver_node => #<LiteralNode @obj => 1>@method_id => :+@argument_nodes => [ #<LiteralNode @obj => 2>]>]>>

# “1” というプログラムのノード#<ProgramNode

@seq_node => #<SequenceNode @nodes =>[ #<LiteralNode @obj => 1>]>>

Page 75: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ヒント:ラベルの使い方

•ラベルはユニークである必要がある• label :begin がプログラム中に 2 箇所出てきたら、どっちに飛べばいいかわからなくなる

→ 固定シンボルの代わりに、ユニークなラベルを生成する gen_label() というメソッドを使う(各スケルトンに準備)

Page 76: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

ヒント:メソッドボディの作り方

•どのスケルトンでも、ast2iseq() の最初で、DefBody だったら method body を作るようになっている

•yasm.rb の define_method_macro() で何をやっているかチェック。

Page 77: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

発展課題最適化

Page 78: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

コンパイルした結果の命令列をチェック

•(多分)手でコンパイルした結果よりも、非効率なコードが生成されるはず。• ast2iseq() は、基本的には子ノードしか見ない。•広い視点で見ると、もっと効率的なことも。

Page 79: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

発展課題B最適化

•発展課題:最適化処理を追加してみよう。•どのレベルで行うか?

•ノード単位で変換?

• ISeq単位で変換?

•何に最適化するか?

• fib() の結果で最適化結果を確認

Page 80: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

発展課題Bもっといろいろなプログラムに対応

•発展課題Aのような、対応文法拡張

•やり方•発展課題Aと同じように調べる• ruby2ast.rb を拡張• ruby_nodes.rb にノードを追加

Page 81: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

そしてこれから

Page 82: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Ruby は広大

•今回対応したのは文法のほんの一部•ほかにもいろいろ•定数、インスタンス変数、グローバル変数、…•正規表現、範囲オブジェクト、…•ブロック、キーワード引数、…•例外•…•…

Page 83: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

Interpret on RubyVM

Ruby 処理系の流れ

83

Rubyscript

Parse

CompileRuby

Bytecode

Object managementGarbage collectorThreading

Embeddedclasses and methods

BundledLibraries

Evaluator

GemLibraries

ASTAbstract Syntax Tree

Page 84: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

これからの参考文献

•『Rubyソースコード完全解説』by @minero-aoki•完全ガイドだけど 1.8 向け。ウェブで全文公開。• 1, 2 部(GC やパーサー)は今でも十分に役に立つ。

•『 Rubyのしくみ Ruby Under a Microscope』 by Pat Shaughnessy• Ruby 2.0 向けの話。•完全網羅ではないが、ガチ勢じゃなければそこそこ役に立つ。

•『RubyでつくるRuby ゼロから学びなおすプログラミング言語入門』 by 遠藤侑介• RubyでRubyインタプリタを作る話。• VMとASTをRubyで作り、それ自身を開発したRubyインタプリタで動作させる。

Page 85: Cookpad 17 day Tech internship 2017 言語処理系入門 Rubyをコンパイルしよう

興味があったらRubyインタプリタの開発に参加してね!