💎

RubyKaigi のセッションを楽しむための予習会をした - Ruby 処理系の基礎知識編

2023/05/09に公開

Leaner 開発チームの黒曜(@kokuyouwind)です。

明後日には RubyKaigi 2023 が始まりますね!自分は LT を採択してもらえて 2 番手で話すので、ぜひ聴きに来てください!

https://rubykaigi.org/2023/presentations/lt/

前回記事 で紹介したとおり、弊社には今回始めて RubyKaigi に参加するメンバーも数名いるため、社内で RubyKaigi の予習会を行いました。

https://zenn.dev/leaner_dev/articles/20230502-rubykaigi-instruction

今回はこのときに話した内容のうち、「Ruby 処理系の基礎知識」の部分を記事にまとめます。

Ruby 処理系の種類

Ruby のソースコードを実行するには Ruby 処理系を利用します。Ruby 処理系にはいくつかの種類があり、それぞれに開発が行われています。

https://www.ruby.or.jp/ja/tech/install/ruby/implementations.html

以下に、 RubyKaigi で触れられることの多い処理系をいくつか紹介します。

MRI(CRuby)

https://www.ruby-lang.org/ja/

MRI(CRuby とも呼ばれる)は、 C 言語で実装された、公式の Ruby 処理系です。MRI は Matz' Ruby Implementation の略で、 Ruby の生みの親である Matz が作った実行系という意味です。一方の CRuby という名前は C 言語で書かれた処理系だからですね。

タイトルに処理系の種類が入ってないセッションはだいたい CRuby の話です。また Tips and Tricks for working in the MRI Codebase は CRuby のコードベースについてのセッションなので、 C 言語の話が含まれています。

https://rubykaigi.org/2023/presentations/jemmaissroff.html#may12

他に、毎回定番となっている Ruby Committers and The World では CRuby のコミッター[1]が集まって様々な話を取り上げるので必見です。

https://rubykaigi.org/2023/presentations/rubylangorg.html#day3

JRuby

https://www.jruby.org/

JRuby は Java で実装された Ruby 処理系です。 JVM で動くため、 Java の動くデバイス上であればどこでも動かせることと、既存の Java 資産を活用できるのが強みですね。

JRuby: Looking Forward は JRuby の現状と将来についての話をするようです。 Java 実装の話に踏み込む内容ではなさそうなので、初学者でも比較的聞きやすそうですね。

https://rubykaigi.org/2023/presentations/headius.html#may12

mruby

https://mruby.org/

mruby は組み込み機器向けに軽量化した Ruby 処理系です。 CRuby と同じく C 言語で実装されていますが、軽量化のため一部機能が制限されています。

UTF-8 is coming to mruby/c は mruby をさらに省メモリ化した mruby/c についての話です。 mruby 関連セッションでは PIC などマイコンの話が絡んでくることが多いのですが、今回は文字コードの話なのでそこまで踏み込まなさそうです。

https://rubykaigi.org/2023/presentations/ima1zumi.html#may11

複数の実行系に触れるセッション

Ruby vs Kickboxer - the state of MRuby, JRuby and CRuby は上記の 3 処理系を全部使ったシステムの話みたいです。タイトルからして面白そうですね。

https://rubykaigi.org/2023/presentations/saramic.html#may13

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 では字句解析と構文解析がややこしく結びついてしまって大変」と思っておけば、セッションを聞く際の前知識としては十分でしょう。

https://yui-knk.hatenablog.com/entry/2023/04/04/190413

さて、 RubyKaigi 2023 ではそんな字句解析・構文解析に関するセッションが豊富です。個人的に興味のある分野なので、どのセッションも非常に楽しみですね。

https://rubykaigi.org/2023/presentations/spikeolaf.html#may11

https://rubykaigi.org/2023/presentations/kddnewton.html#may12

https://rubykaigi.org/2023/presentations/coe401_.html#may12

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はブロックを扱うメソッドのコンパイルを最適化する話のようです。

https://rubykaigi.org/2023/presentations/eregontp.html#may12

Ruby VM(YARV) での実行・JIT

最終的に、ここまでで得られた ISeq を実行します。これを担うのが Ruby 仮想マシンであるYARVです。

YARV はバイトコードを読み込んでそのまま処理するため、一種のインタプリタといえます。それに対し、バイトコードを必要に応じて機械語に変換することで実行を高速化するのが Just-in-time Compiler(JIT) です。

このレイヤーの話は C 言語やメモリレイアウトの話が絡むため非常に難しいです[4]。多分全部理解して聞いている人のほうが珍しいので、初学者の方は「なるほど、こういう難しいことやってる人たちがいるんだなぁ…」くらいの気持ちで聞きつつ分かる範囲を噛み砕いて理解するのが良さそうです。

RubyKaigi 2023 では VM レイヤーでのロックの扱いやガベージコレクションについてのセッションがありそうです。

https://rubykaigi.org/2023/presentations/KnuX.html#may11

https://rubykaigi.org/2023/presentations/eightbitraptor.html#may11

また JIT 絡みでは 3 セッションがあり、特に k0kubun さんのRuby JIT Hacking Guideは Ruby で書いた新しい RJIT を紹介されるようです。

https://rubykaigi.org/2023/presentations/maximecb.html#may12

https://rubykaigi.org/2023/presentations/alanwusx.html#may12

https://rubykaigi.org/2023/presentations/k0kubun.html#may12

もっと詳しく知りたい方への推薦図書

プログラム実行の流れをもっと掘り下げて知りたい方には以下の書籍がおすすめです。

RubyでつくるRuby ゼロから学びなおすプログラミング言語入門 は Ruby を使って Ruby のインタプリタを作る書籍で、実際に手を動かして処理系の仕組みを学ぶことができます。やさしい説明で構文木などの概念を一通り学べるので、初学者が処理系の仕組みを学ぶのに特に適しています。

https://www.lambdanote.com/products/ruby-ruby

Rubyのしくみ Ruby Under a Microscope は CRuby の処理系の詳細を解説した書籍です。 YARV の内部スタックレベルまで踏み込むため、 CRuby の内部の仕組みをしっかり知りたい方が読むのに良いでしょう。

https://tatsu-zine.com/books/ruby-under-a-microscope-ja

まとめ

最後のほうは自分が詳しくないのと力尽きて若干駆け足になりましたが、 Ruby 処理系の概要をまとめました。

特に構文解析周りと JIT 周りは今回セッションが多いため、どういう概念があるのか知っておくことでセッションを聞きやすくなるはずです。

それでは RubyKaigi 楽しみましょう!

脚注
  1. コミットする人、つまりは CRuby 処理系のメインとなる開発者です。 RubyKaigi で修飾語なしにコミッターと言った場合は、大体 CRuby のコミッターを指しています。 ↩︎

  2. 実際には、 CRuby では字句解析と構文解析を厳密に区別せず実装されています。ここではトークン列の概念がわかりやすいよう分けて書いています。 ↩︎

  3. わかりやすさを優先して色々端折っています。 ↩︎

  4. 正直 RubyVM や JIT まわりは自分も全然理解しきれていないため、もし説明に間違いがあったらコメントなどでご指摘いただけると助かります ↩︎

リーナーテックブログ

Discussion