RubyKaigi のセッションを楽しむための予習会をした - Ruby 処理系の基礎知識編
Leaner 開発チームの黒曜(@kokuyouwind)です。
明後日には RubyKaigi 2023 が始まりますね!自分は LT を採択してもらえて 2 番手で話すので、ぜひ聴きに来てください!
前回記事 で紹介したとおり、弊社には今回始めて RubyKaigi に参加するメンバーも数名いるため、社内で RubyKaigi の予習会を行いました。
今回はこのときに話した内容のうち、「Ruby 処理系の基礎知識」の部分を記事にまとめます。
Ruby 処理系の種類
Ruby のソースコードを実行するには Ruby 処理系を利用します。Ruby 処理系にはいくつかの種類があり、それぞれに開発が行われています。
以下に、 RubyKaigi で触れられることの多い処理系をいくつか紹介します。
MRI(CRuby)
MRI(CRuby とも呼ばれる)は、 C 言語で実装された、公式の Ruby 処理系です。MRI は Matz' Ruby Implementation の略で、 Ruby の生みの親である Matz が作った実行系という意味です。一方の CRuby という名前は C 言語で書かれた処理系だからですね。
タイトルに処理系の種類が入ってないセッションはだいたい CRuby の話です。また Tips and Tricks for working in the MRI Codebase は CRuby のコードベースについてのセッションなので、 C 言語の話が含まれています。
他に、毎回定番となっている Ruby Committers and The World では CRuby のコミッター[1]が集まって様々な話を取り上げるので必見です。
JRuby
JRuby は Java で実装された Ruby 処理系です。 JVM で動くため、 Java の動くデバイス上であればどこでも動かせることと、既存の Java 資産を活用できるのが強みですね。
JRuby: Looking Forward は JRuby の現状と将来についての話をするようです。 Java 実装の話に踏み込む内容ではなさそうなので、初学者でも比較的聞きやすそうですね。
mruby
mruby は組み込み機器向けに軽量化した Ruby 処理系です。 CRuby と同じく C 言語で実装されていますが、軽量化のため一部機能が制限されています。
UTF-8 is coming to mruby/c は mruby をさらに省メモリ化した mruby/c についての話です。 mruby 関連セッションでは PIC などマイコンの話が絡んでくることが多いのですが、今回は文字コードの話なのでそこまで踏み込まなさそうです。
複数の実行系に触れるセッション
Ruby vs Kickboxer - the state of MRuby, JRuby and CRuby は上記の 3 処理系を全部使ったシステムの話みたいです。タイトルからして面白そうですね。
CRuby でソースコードが実行されるまでの流れ
RubyKaigi では CRuby についてのセッションが多く、処理系の詳細に踏み込んだ話も出てきます。
この際、処理系がどういう流れでソースコードから実際の動作に結びつけているかの全体像を知っておくと、どのあたりの話をしているのか理解しやすくなります。
処理系がコードを実行する大まかな流れは以下のとおりです。[2]
以降、各ステップについて説明していきます。
字句解析
ソースコードは単なる文字列なので、 1 文字単位ではキーワードの一部なのか、変数の一部なのか、あるいは演算子なのかといった区別をつけることができません。このままだと 変数 = 式
といった構文規則に当てはめるのが難しくなってしまいます。
そこで、ソースコードを Ruby の構文上意味のある単位の並びに変換する処理が 字句解析(Lexical Analysis) です。この単位を トークン と呼び、トークンの並びを トークン列 と呼びます。字句解析はソースコードをトークン化する処理なので トークナイズ(Tokenize) と呼ばれることもあります。
例えば hoge=123
というソースコードは ['h', 'o', 'g', 'e', '=', '1', '2', '3']
という文字の並びに過ぎません。これを [tIDENTIFIER('hoge'), tOP_ASGN, tINTEGER(123)]
というトークン列に変換するのが字句解析です。これによって変数 hoge
、代入演算子、整数リテラル 123
というトークン単位で扱えるようになります。
Ruby では Ripper.lex を利用して字句解析を行えます。ただし後述しますが、 CRuby 実行系では Ripper を使って字句解析をしているわけではなく、構文解析と密結合した形で実装されています。
構文解析
トークン列では各トークン単位での意味はまとまっていますが、相互のつながりがなく式の結合やクラス定義の範囲などが判別しづらくなっています。
そこで文や式の構造がわかりやすいよう、トークン列を木構造のデータに変換する処理が 構文解析(Parsing) です。この結果作られる木構造のデータを 抽象構文木(Abstract Syntax Tree, AST) と呼びます。
hoge=123
の例であれば、以下のような抽象構文木に変換されます。[3]
ルートが代入ノードになっていて、代入ノードの子要素として hoge
という変数と 123
という値が紐付いている形になっています。これにより、トークン同士がどう結びついているのかがわかるようになりました。
Ruby では RubyVM::AbstractSyntaxTree を利用して抽象構文木を得ることができます。
CRuby における字句解析と構文解析
ここまで字句解析と構文解析の一般的な説明をしましたが、実は Ruby では両者のステップは区別されず、密結合して実行されます。
両者の処理は parse.y に記述されています。これは Yacc というパーサジェネレータのための構文規則ファイルで、この記述を元に構文解析器を生成します。
parse.y が難しいことは RubyKaigi で度々話題になっています。どこが難しいのか、なぜこのようになっているのかが Ruby Parser開発日誌 (6) - parse.yのMaintainabilityの話 に書かれています。かなり専門的な話なので、「Ruby では字句解析と構文解析がややこしく結びついてしまって大変」と思っておけば、セッションを聞く際の前知識としては十分でしょう。
さて、 RubyKaigi 2023 ではそんな字句解析・構文解析に関するセッションが豊富です。個人的に興味のある分野なので、どのセッションも非常に楽しみですね。
RubyVM 機械語へのコンパイル
言語によっては抽象構文木をそのまま実行することもありますが、 Ruby では仮想マシン向けの機械語にコンパイルしてから仮想マシンが実行します。
一般に仮想マシン向けの機械語はバイトコードと呼ばれますが、Ruby ではバイトコードに付加情報を加えた InstructionSequence(ISeq) を作ります。
RubyVM::InstructionSequence を利用して ISeq の操作が行なえます。 hoge = 123
の例では以下のようになります。
> iseq = RubyVM::InstructionSequence.compile('hoge = 123')
=> <RubyVM::InstructionSequence:<compiled>@<compiled>:1>
> puts iseq.disasm
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,10)> (catch: FALSE)
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] hoge@0
0000 putobject 123 ( 1)[Li]
0002 dup
0003 setlocal_WC_0 hoge@0
0005 leave
Ruby の仮想マシンなので、オブジェクトをそのまま扱えるようになっています。オブジェクトをスタックに積む putobject
命令が確認できますね。
このフェイズを扱うセッションは少なそうですが、Splitting: the Crucial Optimization for Ruby Blocksはブロックを扱うメソッドのコンパイルを最適化する話のようです。
Ruby VM(YARV) での実行・JIT
最終的に、ここまでで得られた ISeq を実行します。これを担うのが Ruby 仮想マシンであるYARVです。
YARV はバイトコードを読み込んでそのまま処理するため、一種のインタプリタといえます。それに対し、バイトコードを必要に応じて機械語に変換することで実行を高速化するのが Just-in-time Compiler(JIT) です。
このレイヤーの話は C 言語やメモリレイアウトの話が絡むため非常に難しいです[4]。多分全部理解して聞いている人のほうが珍しいので、初学者の方は「なるほど、こういう難しいことやってる人たちがいるんだなぁ…」くらいの気持ちで聞きつつ分かる範囲を噛み砕いて理解するのが良さそうです。
RubyKaigi 2023 では VM レイヤーでのロックの扱いやガベージコレクションについてのセッションがありそうです。
また JIT 絡みでは 3 セッションがあり、特に k0kubun さんのRuby JIT Hacking Guideは Ruby で書いた新しい RJIT を紹介されるようです。
もっと詳しく知りたい方への推薦図書
プログラム実行の流れをもっと掘り下げて知りたい方には以下の書籍がおすすめです。
RubyでつくるRuby ゼロから学びなおすプログラミング言語入門 は Ruby を使って Ruby のインタプリタを作る書籍で、実際に手を動かして処理系の仕組みを学ぶことができます。やさしい説明で構文木などの概念を一通り学べるので、初学者が処理系の仕組みを学ぶのに特に適しています。
Rubyのしくみ Ruby Under a Microscope は CRuby の処理系の詳細を解説した書籍です。 YARV の内部スタックレベルまで踏み込むため、 CRuby の内部の仕組みをしっかり知りたい方が読むのに良いでしょう。
まとめ
最後のほうは自分が詳しくないのと力尽きて若干駆け足になりましたが、 Ruby 処理系の概要をまとめました。
特に構文解析周りと JIT 周りは今回セッションが多いため、どういう概念があるのか知っておくことでセッションを聞きやすくなるはずです。
それでは RubyKaigi 楽しみましょう!
Discussion