投稿

CRubyの内部構造:パーサー・コンパイラ・YARV・GC

CRubyの内部構造:パーサー・コンパイラ・YARV・GC

Rubyコードが実行されるまでの流れと、CRubyを構成するコンポーネントを整理する。

実行フロー全体像

Rubyコード
    ↓ Lrama(パーサー)
AST(抽象構文木)
    ↓ コンパイラ
バイトコード
    ↓ YARV(VM)
CPU命令

※ GCはYARV実行中に並走してメモリを管理

ruby hoge.rb を叩いた瞬間に上から順番に走る。

各コンポーネントの役割

パーサー(Lrama)

Rubyコードを読み取り、構文解析してASTを生成する。Ruby 3.2からLramaが採用された(以前はBison)。

AST(抽象構文木)

コードの構造をツリー状に表現したもの。コンパイラが処理しやすい中間形式。

x = 1 + 2
代入
├── 変数: x
└── 足し算
    ├── 1
    └── 2

コンパイラはこのツリーを走査してバイトコードを生成する。

コンパイラ

ASTをYARVが解釈できるバイトコードに変換する。固有の名称はなく「CRubyのコンパイラ」と呼ばれる。

バイトコード vs アセンブリ

似ているが別物。

  誰が読む
バイトコード VM(ソフトウェア) YARV命令
アセンブリ CPU(ハードウェア) x86, ARM命令

バイトコードはCPUに直接渡せない。YARVがバイトコードを読んで、最終的にCPUが理解できる命令に変換して実行する。

バイトコードの中身はRubyから確認できる:

puts RubyVM::InstructionSequence.compile("x = 1 + 2").disasm
== disasm: #<ISeq:<compiled>@<compiled>:1>
0000 putobject_INT2FIX 1        # 1をプッシュ
0002 putobject_INT2FIX 2        # 2をプッシュ
0004 opt_plus                   # 足す
0005 setlocal x                 # xに代入

YARV(Yet Another Ruby VM)

バイトコードを受け取って実行する機械。コンパイルは担当しない。

YARVの本質はバイトコードを1命令ずつ読んで処理するループ:

while (命令がある) {
    switch (命令の種類) {
        case ADD:   スタックから2つ取り出して足してプッシュ; break;
        case PUSH:  値をスタックに積む; break;
        case STORE: 変数に値を保存; break;
    }
}

GC(ガベージコレクター)

YARV実行中に並走してメモリを管理する。CRubyのデフォルトはトライカラーインクリメンタルGC。Ruby 3.2からはYJITとの組み合わせも可能。

GCの仕事は「もう誰も使っていないオブジェクトを見つけてメモリを解放すること」。

マーク&スイープの基本動作:

// ① 全オブジェクトを「未使用」にリセット
for (全オブジェクト) { obj.marked = false; }

// ② 今使っているものからたどれるオブジェクトに印をつける
mark(root_objects);

// ③ 印がついていないものを解放
for (全オブジェクト) {
    if (!obj.marked) free(obj);
}

Rubyコードを書く側はGCを意識しないが、オブジェクトが生成されるたびに内部でmallocが走り、GCがfreeのタイミングを管理している。

CRuby全体構造

CRuby(Cで書かれたRubyの実装)
├── Lrama(パーサー)
├── コンパイラ
├── YARV(VM)
└── GC

これら全てがCRubyというひとつのC実装の中に収まっている。

トレンドのタグ