こんにちは!
普段はRubyとRailsを扱っている@westtailです!
RubyKaigiに初参加で聞いていましたが、正直に押さえておかないと楽しめない知識部分があると思い、以下に記載していきたいと思います
記事の内容に誤りや不適切な点がございましたら、コメントでやさしくご指摘いただけると嬉しいです🙏
この記事は技術面に絞ってキーワード・知識をメインに初参加向けに記載します
前編では実際に参加した流れや感想をメインに記載しました!
前半の記事はこちら!
結論:これを参加前に見たかった
Rubyの実行までの流れやJITなど網羅されていて、正直これ見ればOK!
ですので、もう少し噛み砕いて欲しいよって感じで、自分自身に向けても記載してます!
Rubyって何?
Rubyは、まつもとゆきひろ(Matz)氏によって開発されたオブジェクト指向スクリプト言語になります
Rubyは絶妙にバランスのとれた言語です。 Rubyの作者である、Matzことまつもと ゆきひろ氏は、好みの言語(Perl、Smalltalk、Eiffel、Ada、Lisp)の一部をブレンドし、 関数型プログラミングと命令型プログラミングが絶妙に調和された新しい言語を作りました。
Rubyの特徴
- 数値、文字列、nil(無)まで、すべてのデータがオブジェクト
- 動的型付け言語
- 直感的で読みやすい構文
- 強力なメタプログラミング機能
RubyKaigi 注目すべきキーワード・分野
大きく4つになると思います
- Rubyのソースコードから実行までの流れ
- JITの歴史と次世代
-
Rubyの機能キーワード
処理系・パフォーマンス系(GC、Ractor、Fiber Schedulerなど)
開発体験系(RBS・LSPなど)
実験的な言語機能(Ruby::Boxなど) -
Rubyのさまざまな処理系
CRuby / JRuby / mruby / PicoRuby
それぞれの事柄に関して説明していきます
1. Rubyのソースコードから実行までの流れ
処理の流れについてプログラミング言語全般からRubyの順に説明します
また出てくるキーワードについても説明します
プログラミング言語のソースコードから実行までの流れ
ソースコードからASTまでは多くの言語で共通、その後は実行方式で2つに分かれます
- 機械語に変換されてCPUが直接実行
- バイトコードに変換されてVMが実行
実行時の処理の流れ
【ソースコード】:書かれたプログラムの文字列
例)`a = 1 + 2`
↓
レクサー(字句解析):意味のある最小単位に分割する処理
↓
【トークン列】:何の単語か・役割がわかる形に分割された列
例)`a` / `=` / `1` / `+` / `2`
↓
パーサー(構文解析):文法ルールに従って木構造に変換する処理
↓
【AST(抽象構文木)】:コードの構造と関係を図にしたもの
=(代入)
/ \
a +(加算)
/ \
1 2
↓
ここまでは多くの言語で共通
↓
コンパイラ:ASTをコンピュータが実行できる形に変換するプログラム
↓ ※言語によって出力される形式が異なる
↓
├─ 【機械語】:0と1のバイナリ
│ ↓
│ CPUが直接実行(C, Rust など)
│
└─ 【バイトコード】:中間的な命令列
↓
VMが実行(Ruby, Python, Java など)
VM(仮想マシン):バイトコードを解釈してCPUに伝える通訳者のようなソフトウェア
Rubyの実行までの流れ
具体的にRubyでは以下のようになります
【ソースコード】書かれたプログラムの文字列
例)`a = 1 + 2`
↓
レクサー(字句解析):意味のある最小単位に分割する処理
↓
【トークン列】何の単語か・役割がわかる形に分割された列
例)`a` / `=` / `1` / `+` / `2`
↓
パーサー(構文解析):文法ルールに従って木構造に変換する処理
↓
【AST(抽象構文木)】コードの構造と関係を図にしたもの
=(代入)
/ \
a +(加算)
/ \
1 2
↓
コンパイラ:ASTをコンピュータが実行できる形に変換するプログラム
↓
【ISeq(Instruction Sequence)】Rubyにおけるバイトコード YARVへの指示書のようなもの
↓
YARV(Yet Another Ruby VM):ISeqを解釈してCPUに伝える通訳者のようなソフトウェア
↓
実行
ASTからISeqに変換され、さらにYARVにて実行されます
出てきた・知ってるといいかもキーワード
レクサー(字句解析)
文字列を単語に分割する処理
x = 1 + 2
→ `x`(識別子)/ `=`(代入演算子)/ `1`(整数)/ `+`(演算子)/ `2`(整数)
lex state
レクサー(字句解析器)が「今どういう状態か」を表す内部フラグ
Rubyは文脈によって同じ文字の意味が変わるので「今どういう文脈か」を状態として持つ必要がある
def foo(x)
-x # ここの - はマイナス(単項演算子)
end
a - b # ここの - は引き算(二項演算子)
パーサー(構文解析)
コードを「意味のある構造(AST)」に変換するもので、トークンに「構造・意味」を与える
`x`(識別子) / `=`(代入演算子) / `1`(整数) / `+`(演算子) / `2`(整数)
↓
=(代入)
/ \
x +(加算) ← 足し算が先、という構造を理解
/ \
1 2
LALR(Look-Ahead LR parsing)
パーサーのアルゴリズムの一種
トークンを左から順に読みながら、1つ先のトークンを先読みして
「次に何が来るか」を判断しながら構文解析を進める方式
LR パーサー
Left-to-right, Rightmost derivationの略
左から右にトークンを読みながら、右端から構文を導出していく方式
1965年にDonald Knuthが提唱した構文解析の手法
Pratt パーサー
1973年に Vaughan Pratt が提案したパーサーの手法
特徴:演算子の優先度を演算子自体に持たせる設計で、シンプルで拡張しやすい
例)`1 + 2 * 3` の場合
`*` が `+` より優先度が高いことを `*` 自身が知っているので
→ `1 + (2 * 3)` と正しく解釈できる
BNF(Backus-Naur Form)
プログラミング言語の文法ルールを定義するための記法
「この言語ではどんな書き方が文法的に正しいか」をルールとして表現
パーサー(構文解析)のベースとなるルール定義として使われる
例)
<式> ::= <数> | <式> + <式> | <式> * <式>
→「式とは、数字 または 式+式 または 式*式 のいずれかである」という意味
Prism
Ruby 3.3から実験的サポートとして追加され、Ruby 3.4でデフォルトパーサーになったShopify製の新しいパーサー
旧来の parse.y ベースのパーサーを置き換えることを目指して開発された
Prismは手書きパーサー(パーサージェネレーターに頼らず人間が直接書いたパーサー)のため、自動生成に比べ柔軟性が高く、エラーメッセージなども細かく制御できる
特徴
└ より正確・高速
└ エラーメッセージが改善
└ 手書きパーサーなので柔軟性が高い
└ RuboCop・Ruby LSPなどのツールもPrism対応へ移行中
AST(Abstract Syntax Tree / 抽象構文木)
パーサーがトークン列から作り出す木構造
コードの「構造と関係」を表したもの
例)`x = 1 + 2` のAST
=(代入)
/ \
x +(加算)
/ \
1 2
LST(Lossless Syntax Tree)
ソースコードの全情報を一切捨てずに保持する構文木
ASTの場合
「プログラムの意味」だけを抽出するため、捨てるものが発生
例)`x = 1 + 2 # 足し算`
ASSIGN
├── x
├── PLUS
│ ├── 1
│ └── 2
└── ※スペース・コメントは捨てる
ASTが捨てる情報
├── 空白・インデント
├── コメント
├── 改行の位置
├── カッコの有無(意味が変わらない場合)
└── セミコロン等の区切り文字
LSTの場合
すべての文字がノードとして存在するため、元のコードに完全に戻せる(Lossless)
例)`x = 1 + 2 # 足し算`
ASSIGN
├── x
├── " "(スペース)
├── =
├── " "(スペース)
├── PLUS
│ ├── 1
│ ├── +
│ └── 2
├── " "(スペース)
└── # 足し算(コメント)
コンパイラ
ASTをコンピュータが実行できる形に変換するプログラム
CRubyではYARVコンパイラが担当し、ASTをISeqに変換する
バイトコード
VMが実行するための中間的な命令列
ソースコードをそのまま実行するより速く、ネイティブコードと違いCPUアーキテクチャに依存しない
方法① ソースコードを直接実行(インタープリタ)の場合
毎回文字列を読んで解釈 → 遅い(Ruby 1.8まではこれに近かった)
方法② ネイティブコードに直接コンパイル(C言語等)の場合
速い → ただしCPUのアーキテクチャごとにコンパイルし直す必要がある(x86, ARM64, RISC-V...)
方法③ バイトコード方式はその中間
ソースコード
↓ 一度だけ変換
バイトコード(命令列)
↓ VMが実行
結果
バイトコードを一度変換すれば、あとはVMが命令を順番に実行するだけ
バイトコードはVMさえあればどのCPUでも動く
ISeq(Instruction Sequence)
RubyにおけるバイトコードがISeq
例)`x = 1 + 2` のISeq
putobject 1 # 1をスタックに積む
putobject 2 # 2をスタックに積む
opt_plus # 足し算
setlocal x # 結果をxに代入
Rubyで実際に確認できる
puts RubyVM::InstructionSequence.compile("x = 1 + 2").disasm
VM(Virtual Machine / 仮想マシン)
バイトコードを解釈して実行するソフトウェア
ハードウェア(CPU)の違いを吸収する通訳者のようなもの
代表的なVM
├── YARV → Ruby
├── JVM → Java / Kotlin
└── CPython → Python
YARV(Yet Another Ruby VM)
RubyのバイトコードVM
ISeqを1命令ずつ解釈して実行
例)`x = 1 + 2` の実行
putobject 1 # 1をスタックに積む
putobject 2 # 2をスタックに積む
opt_plus # 足し算(スタックから2つ取り出して結果を積む)
setlocal x # 結果をxに代入
主な命令列の例
| 命令 | 意味 |
|---|---|
putobject |
値をスタックに積む |
opt_plus |
足し算 |
opt_minus |
引き算 |
opt_mult |
掛け算 |
setlocal |
ローカル変数に代入 |
getlocal |
ローカル変数を取得 |
send |
メソッドを呼び出す |
leave |
処理を終了して戻る |
2. JITの歴史と次世代
そもそもJITとは何かの説明とその改良版について
JIT(Just-In-Time コンパイラ)
プログラムの実行中にホットスポット(よく実行される箇所)を機械語に変換して高速化する仕組み
通常の実行(JITなし)の場合
Rubyコード
↓
バイトコード(ISeq)
↓
YARVが1命令ずつ解釈して実行 ← 毎回解釈するので遅い
JITありの実行の場合
Rubyコード
↓
バイトコード(ISeq)
↓
YARVが実行しながら監視
↓
「このメソッド、めちゃくちゃ呼ばれてるな」← ホットスポット検出
↓
その部分だけ機械語に変換してキャッシュ
↓
次回からキャッシュした機械語を直接実行 ← 爆速!
ポイント
- 全部を機械語にするのではなく「よく使う部分だけ」変換
- 変換コストがあるので最初は遅くなることもある
- 実行時の情報を使って最適化できる(事前コンパイルにない強み)
MJIT(Method-based JIT compiler)
Ruby 2.6で導入されたRuby初の公式JIT
メソッド単位でCコードを生成し、gccやclangでコンパイルして実行する方式
仕組み
バイトコード(ISeq)
↓
メソッド単位でC言語のコードを生成
↓
gccやclangで機械語にコンパイル
↓
実行
特徴
- Cコードを経由するためコンパイルが遅い
- 起動時のウォームアップに時間がかかる
- Ruby 3.3でRJITに置き換えられ廃止
YJIT(Yet Another Just-In-Time compiler)
Shopifyが開発し、Ruby 3.1から本体に統合された現在の本番実用JIT
MJITとの違いと特徴
- MJITはメソッド単位でCコードを生成 → コンパイルが遅い
- YJITはBasic Block単位で機械語に変換 → 速い・効率的
- Rustで実装されており安全性が高い
- 現在はZJITへの移行も議論されている
RJIT(Ruby-based JIT compiler)
Ruby 3.3でMJITの後継として導入されたJIT
MJITと異なりCではなくRubyで実装されている
MJITとの違いと特徴
- MJIT → Cコードを生成してgcc/clangでコンパイル
- RJIT → Ruby実装
- 本番利用よりも学習・実験目的の位置づけ
- 本番ではYJITを使うことが推奨されている
ZJIT
YJITの次世代版としてRuby 4.0から実験的に導入された新しいJIT
YJITとの違いと特徴
- YJIT → Basic Block単位でコンパイル
- ZJIT → メソッド単位でコンパイル ← より広い範囲に最適化できる
- IRと呼ばれる中間表現を持つ設計
- 本番利用に向けてさらなる改善が進んでいる
3. Rubyの機能キーワード
Ractor
Ruby 3.0で導入された並列処理の仕組み
メモリを共有しない独立した実行単位で、並列処理が可能
特徴
- Ractor間でオブジェクトを共有できない(安全性のため)
- メッセージパッシングでRactor間のやり取りをする
- まだ実験的な機能で本番利用はこれから
GVL(Global VM Lock)
- 通常のRubyスレッドはGVLにより同時に1つしか実行できない
- Ractorはこの制約を超えて並列実行できる
Fiber Scheduler
Ruby 3.0で導入された、Fiberを使ったノンブロッキングI/Oの仕組み
従来の問題とFiber Schedulerの解決
- I/O待ち(DB・HTTP・ファイル)の間、スレッドがブロックされて無駄が発生
- I/O待ちの間に別のFiberに切り替えて処理を継続
- スレッドをブロックせず効率的にI/Oを処理できる
RBS(Ruby Signature)
Ruby 3.0から標準添付された型シグネチャの記述言語
Rubyのコードとは別ファイルに型情報を書く
例)
# rbs
def greet: (String name) -> String
特徴
- Rubyコード本体は変更不要
- 型チェッカー(Steep)と組み合わせて使う
LSP(Language Server Protocol)
エディタと言語解析ツールをつなぐ共通の通信規格
Microsoftが提唱し、VS CodeやNeovimなど多くのエディタが対応
RubyではShopify製のRuby LSPが開発されており
補完・定義ジャンプ・型情報の表示などの開発体験向上に貢献
LSP以前
エディタごとに補完・定義ジャンプなどを個別実装が必要
LSP以後
Language Serverを1つ作れば対応エディタすべてで動く
Ruby::Box
同じRubyプロセス内に「定義が独立した小部屋」を作れる仕組み
Ruby 4.0から実験的機能として導入された
何が嬉しいか
- 同じgemの別バージョンを同一プロセス内で共存できる
- プロセスを分けずに隔離できるので軽量
分離されるもの(Boxごとに独立)
- クラス・モジュールの定義
- 定数
- requireの管理
[ Ruby Process ]
├── [ Box A:本番アプリ ]
│ └── String#+ 正常のまま
│
└── [ Box B:隔離空間 ]
└── モンキーパッチここだけ・別バージョンgemもOK
GC(Garbage Collection)
プログラムが使わなくなったメモリを自動的に回収する仕組み
流れ
オブジェクト生成
↓
GCが定期的に「まだ使われているか」を確認
↓
使われていないオブジェクトを検出
↓
メモリを回収・解放
GCがない場合は開発者が手動でメモリを解放する必要がある(C言語など)
- 解放忘れ → メモリリーク
- 二重解放 → クラッシュ
RDoc(Ruby Documentation)
Rubyのソースコードに書いたコメントから
ドキュメントを自動生成するツール
FFI(Foreign Function Interface)
異なる言語間で関数を呼び出す仕組み
RubyからC言語などで書かれた外部ライブラリを呼び出せる
画像処理・暗号化・OSのシステムコールなど、難しい低レベルな処理を使いたい場合や既存のCライブラリの資産をそのまま活用したい場合に利用
C拡張との違い
C拡張
- Cでコードを書いてコンパイル
- 速いけど面倒・危険
FFI
- Rubyから直接Cライブラリを呼ぶ
- コンパイル不要で書きやすいが、少し遅い
4. Rubyのさまざまな処理系
処理系にもいくつか種類があります
CRuby(MRI)
Rubyの公式リファレンス実装
C言語で書かれており、通常「Ruby」といえばCRubyを指す
MRI(Matz's Ruby Interpreter)とも呼ばれる
hoge.rb(ソースコード)
↓ ① 字句解析・構文解析(Prism)
AST(抽象構文木)
↓ ② コンパイル
YARV命令列(バイトコード)
↓ ③ 実行
YARV(Yet Another Ruby VM)
JRuby
JVM(Java Virtual Machine)上で動作するRubyの実装
Charles Nutter氏らが開発
hoge.rb(ソースコード)
↓ ① 字句解析・構文解析
AST(抽象構文木)
↓ ② コンパイル
Javaバイトコード
↓ ③ 実行
JVM(Java Virtual Machine)
mruby
組み込みシステム向けに設計された軽量Ruby実装
Rubyの作者まつもとゆきひろ(Matz)氏が主導
CRubyは数十MBのRAMやOSが必要だが、マイコン(ESP32など)はRAMが数百KBしかないため対応できるmrubyが生まれた
特徴
- フットプリントが小さい
- OSなしでも動作する
- C言語へのコンパイル(mrbc)が可能
- IoTデバイス・ゲームエンジンへの組み込みに使われる
PicoRuby
mrubyよりさらに小さいマイコン向けのRuby実装
Raspberry Pi PicoやESP32など超小型マイコンでRubyが動く
hasumikin(Hitoshi HASUMI)氏が開発
mrubyとの違い
- mrubyよりさらにフットプリントが小さい
- Raspberry Pi Pico(RAM 264KB)でも動作
ruby.wasm
RubyをWebAssemblyにコンパイルしたものでブラウザ上でRubyが動く
仕組み
Ruby(CRuby)
↓ WASMにコンパイル
ruby.wasm
↓ ブラウザで読み込む
ブラウザのWASMランタイム上でRubyが動く
CRuby.WASMの問題
ブラウザ(シングルスレッド)
└─ CRubyがカーネルスリープを呼ぶ → ブラウザごとフリーズ
カーネルスリープはOSに処理を渡す命令なので、ブラウザ環境では使えない
PicoRuby.WASMの解決
ブラウザ
└─ PicoRuby → カーネルスリープを使わない設計
→ ブラウザのイベントループに乗っかる → フリーズしない
CRuby.WASM → OS前提の設計 → ブラウザと相性悪い
PicoRuby → 最初からOS無し前提 → ブラウザと相性いい
まとめ
お疲れ様でした、とても長くなってしまいました
Rubyという言語一つとってもいろんなキーワードや技術が利用されています
RubyKaigiの講演ではこれらのキーワードを知ってるとより楽しめると思います
特に英語の講演だとキーワードもわからないと何言っているかわからないですね
RubyKaigiに参加する前の復習の手助けになれば幸いです!