こんにちは。プロダクトエンジニアのyudaiです。5/11から開催されるRubyKaigi2023に向けて、社内で勉強会を実施しました。自己学習のきっかけづくりとして、RubyKaigiのトークを聞く準備方法、トークテーマ、RubyKaigiで話題になりがちな技術について広く浅くお話しました。
RubyKaigi 2023に参加される方のお役に立つかもしれませんので、この勉強会で使った資料を公開します。
RubyKaigi 2023 予習資料
留意事項
この記事はyudai(@ytnk531)が、RubyKaigi 2023の予習のために調べた内容をまとめたものです。誤った内容や古い内容が多く含まれる可能性があることをご留意ください。誤りなどについてご指摘を頂ける場合は、TwitterのDMかリプライなどでご連絡をいただけると大変助かります。
内容
- 事前に理解していたほうが良さそうなRubyの仕組み(パーサ、JIT、並行処理あたり)の紹介
- ここ数年でRubyKaigiで話題になっている内容の紹介
対象
- RubyKaigiに初めて参加する方
- RubyKaigiでいきなりセッションを聞いて理解できるか不安な方
RubyKaigiの準備
- RubyKaigi 2022のMatzさんの発表の後半を聞く. https://youtu.be/m_LW5WIYJ9Q?t=2321
- 興味のある発表のAbstructを読む.
わからない単語があれば調べる
https://rubykaigi.org/2023/schedule/ - 興味のある話題、登壇者に関連する過去の発表を聴く。
過去の発表はYouTubeで視聴できる
https://rubykaigi.org/2022/schedule/
RubyKaigiの話題の分類
- Rubyの未来の話(今後の展望、新文法、新機能)
- Rubyの新機能の詳しい話(実装の詳細、設計、最適化など)(WebAssembly、JIT、型解析…)
- Rubyでこんなこともできる(ようにした)よ、の話(機械学習、mrubyで衛星を制御してみた…)
3は特に予備知識が無くても発表者が説明してくれることが多いので、1, 2を聞くための前提となる話をします。
近年のRubyKaigiで話題になりがちなのもの
- 高速化
- 並列処理、並行処理
- JIT
- メモリ管理の最適化
- Rubyを便利にする
- 静的型解析
- WASM対応
Rubyの基礎
Rubyの処理系
Rubyで書かれたソースコードを実行するためのプログラム。MRI(CRuby), JRuby, TruffleRuby, mrubyなど。
特にことわりが無い場合はMRIのことを指す。MRIはMatz’ Ruby ImplementationまたはMatz Ruby Interpreterの略。C言語で書かれているのでCRuby。
TruffleRuby GraalVM上に実装されたRuby。ベンチマークによってはかなり速い。JVM(Javaを実行するためのVM)由来のHotSpot VMによる最適化と、CRubyにはない独自の最適化が効いているとされている。ただしGraalVMで実装しなおしているので使えるRubyのバージョンが最新ではない。また、今のところ互換性が100%ではない。
TRuffleRubyの互換性
https://github.com/oracle/truffleruby/blob/master/doc/user/compatibility.md
https://eregon.me/blog/2020/06/27/ruby-spec-compatibility-report.html
GraalVMについて
https://www.slideshare.net/tamrin69/getting-started-graalvm
トーク
- https://rubykaigi.org/2023/presentations/headius.html#may12
- https://rubykaigi.org/2023/presentations/ima1zumi.html#may11
CRubyのプログラム実行の流れ
Rubyプログラムは、下記の工程を経てYARV命令列に変換され、YARVで実行される。
- Rubyプログラムを字句解析してトークン列を得る
- トークン列を構文解析してASTノードを得る
- ASTノードをコンパイルしてYARV命令列を得る
YARV
YetAnotherRubyVM。単にRuby VMとも。Rubyのためのスタックマシン型の仮想マシン。YARV命令列を一つずつ処理する。実際のプログラム実行はこのプログラムで行われる。
https://ja.wikipedia.org/wiki/スタックマシン
Rubyプログラム
10.times do |n| puts n end
AST(Absyract Syntax Tree, 抽象構文木)ノード
YARV命令列(インストラクションシークエンス, insns, バイトコードとも)
詳しくはRubyのしくみを参照。
トーク
- https://rubykaigi.org/2023/presentations/coe401_.html#may12
- https://rubykaigi.org/2023/presentations/spikeolaf.html#may11
- https://rubykaigi.org/2023/presentations/kddnewton.html#may12
- https://rubykaigi.org/2023/presentations/flyerhzm.html#may13
過去のトーク
JITコンパイラ
Just In Time Compiler。YARV命令列をYARVに順番に与えるのではなく、最適化されたネイティブコード(機械語)を動的に生成することで高速化する。ネイティブコードにするから速いというよりは、実行時の情報を用いて最適化できるので速くなる。いくつか種類があって、有名なものだとMJIT、YJITがある。
YJIT
- Shopifyがrailsアプリケーションを速くするために開発
- YARV命令列→中間表現(YJIT IR)→ネイティブコード
- x86-64 と arm64/aarch64に対応
- Rustを使っている
- Lazy Basic Bolock Versioning(LBBV)により、実行時の型に特化したコードを生成しつつ、通過する分岐だけをネイティブコードにコンパイルする
- railsアプリケーションでも高速化できる
MJIT
- YARV命令列→C言語のソースコード→ネイティブコード
- メソッド、ブロック単位でネイティブコードにコンパイルする
- メソッドのYARV命令列→対応するCのソースコードをつなぎ合わせる→GCC→実行ファイルをYARVから呼び出す
- Cコードの生成はRubyの
RubyVM::MJIT
行っているので、その気になればモンキーパッチできる
- gccを使ってネイティブコードを生成する
- CPUアーキテクチャの違いはgccで吸収してくれるのでgccでサポートしているアーキテクチャなら動く
- gccでのコンパイルに50ms~200msかかるのでJITの処理はRubyの処理とは別スレッドで動く
- railsアプリケーションだとあまり高速化できない
MIR
- 軽量なJITコンパイラを実装するための中間表現
- YARV命令列からMIRを生成して、MIRから最適化を施したネイティブコードを生成する
- Rubyだけでなく、ほかの言語もこの中間表現を用いてネイティブコードを生成できるようにすることを標榜している
- MRubyと組み合わせることも想定している
https://youtu.be/emhYoI_RiOA?t=972
トーク
- https://rubykaigi.org/2023/presentations/alanwusx.html#may12
- https://rubykaigi.org/2023/presentations/maximecb.html#may12
- https://rubykaigi.org/2023/presentations/k0kubun.html#may13
過去のトーク
- https://rubykaigi.org/2022/presentations/maximecb.html#sep08
- https://rubykaigi.org/2022/presentations/k0kubun.html#sep08
- https://rubykaigi.org/2022/presentations/Kenta Murata.html#sep09
- https://rubykaigi.org/2022/presentations/alanwusx.html#sep10
- https://rubykaigi.org/2022/presentations/vnmakarov.html#sep10
(Object) Shape
Rubyのオブジェクトは、インスタンス変数へのアクセスに時間がかかる。TruffleRubyでは、Object Shapeという技術を使ってこの問題を改善した。CRubyにもShapeを実装しようという流れがある。
トーク
今年はなさそうだがYJITの性能向上にShapeが一役買ったようなので、YJIT関連の発表で触れられるかも。
過去のトーク
- https://rubykaigi.org/2022/presentations/jemmaissroff.html#sep09
- https://rubykaigi.org/2021-takeout/presentations/chrisgseaton.html
並列処理
Rubyの並列処理の機能がここ何年かで追加された。いくつか種類があるので、GVLとあわせて説明する。
下記の記事が詳しい
https://tech.unifa-e.com/entry/2021/01/29/175021
並行と並列の違い
https://zenn.dev/hsaki/books/golang-concurrency/viewer/term#「並行」と「並列」の定義の違い
GVL(GIL)
グローバルVMロック。一般にはグローバルインタプリタロック。
複数スレッドが同時に実行されないようにすることで、スレッドセーフでないコードを複数スレッドで動作させる仕組み。IO待ち(ブロッキング)中に限って同時に実行できる。
Rubyは後述のRactorを利用しないとGVLがかかった状態で動作する。
Thread
カーネルレベルスレッドを使って並行処理する仕組み。1つのスレッドにたいしてプロセッサのスレッドが1つ割り当てられる。GVLがあるので並列処理になるのはIO待ちの間だけ。
Ractor
カーネルレベルスレッドを使って並列に処理する仕組み。Ruby 3.0で追加された。スレッドセーフなコードを書くための仕組みを用意している。
https://techlife.cookpad.com/entry/2020/12/26/131858
Fiber Scheduler
IO待ちで並列処理するのをFiberで実現するための仕組み。FiberはThreadと違ってユーザーレベルスレッドなので、よりも低いコストでIO待ちを並列に処理できる。
複数のFiberがあるときに、あるFiberがIO待ちになると別のFiberの処理を行うようにいい感じに実行順序をコントロールするような実装を書きやすくするためのインターフェースを提供するのがFiber Scheduler。
https://docs.ruby-lang.org/ja/latest/class/Fiber.html
https://chibash.github.io/lecture/os/mt01.html#:~:text=がちである。-,カーネルレベル・スレッドとユーザレベル・スレッド,-スレッドの実装
トーク
- https://rubykaigi.org/2023/presentations/KnuX.html#may11
- https://rubykaigi.org/2023/presentations/ko1.html#may11
- https://rubykaigi.org/2023/presentations/m_seki.html#may12
- https://rubykaigi.org/2023/presentations/ioquatix.html#may13
過去のトーク
- https://rubykaigi.org/2022/presentations/ko1.html#sep08
- https://rubykaigi.org/2022/presentations/ioquatix.html#sep10
メモリ管理
ガベージコレクション(GC)
使っていないメモリを見つけて開放する仕組み。Rubyはマークアンドスイープ方式を利用する。
Rubyのオブジェクトは1つ40バイトで、オブジェクトを記録する領域をブロックと呼ぶ。メモリを確保/解放する単位はヒープページと呼ばれ、16KBの容量を持ち、ヒープページ1つにつき400個ほどのブロックを格納できる。
GCを行っても、使用されているメモリ領域を移動することはできない。このため、メモリの断片化が起こって、隙間なく埋めれば解放できるが実際には解放できないメモリ領域が生まれる。
GC Compaction
GCを行った際にヒープページ間の移動を行い、ヒープページ内にブロックを隙間なく配置できるようにする仕組み。より多くのメモリ(ヒープページ)を開放できたり、メモリの局所性が高まることでCPUのキャッシュ効率が上げられるなどのメリットがある。
https://techracho.bpsinc.jp/hachi8833/2022_06_02/118259
可変幅アロケーション(VWA: variable width allocation)
ブロックサイズ40バイトを超えるデータを、Ruby用のヒープで確保できるようにする仕組み。
この仕組みを使わない場合は、ブロックサイズを超えるデータはRubyのヒープページではないメモリ領域を新たに借りて格納する。
https://techracho.bpsinc.jp/hachi8833/2022_06_08/118447
トーク
過去のトーク
- https://rubykaigi.org/2022/presentations/eightbitraptor.html#sep09
- https://rubykaigi.org/2022/presentations/KnuX.html#sep09
- https://rubykaigi.org/2022/presentations/peterzhu2118.html#sep10
- https://rubykaigi.org/2021-takeout/presentations/peterzhu2118.html
静的型付け
ここ数年、静的型付け言語が隆盛を極めている影響か、Rubyでも型を書けるようにしようという動きがある。
こちらの記事が詳しい
https://techlife.cookpad.com/entry/2020/12/09/120454
RBS
Ruby の型情報を扱う言語。
module ThriftyFileApplier class Error < StandardError end def self.applier: [T] (String applied_time_path, *String source_paths) { () -> T } -> T def self.apply: [T] (String applied_time_path, *String source_paths) { () -> T } -> T # Actual applier class. class Applier[T] def initialize: (String applied_time_path, *String source_paths) { () -> T } -> void def apply: () -> T def exec_if_updated: [U] () { () -> U } -> U private def exec_with_log: [U] (Float time_f) { () -> U } -> U def source_update_time: () -> Time def newest_mtime: (Pathname path) -> Time def last_applied_time_f: () -> Float end end
Steep
Rubyの静的型検査器。RBSを利用する。プログラマが型を記述することを前提としていて、記述した部分について高速に解析できる。LSP対応のLanguage Serverとしても動作する。VSコード拡張がある。
TypeProf
型注釈のない Ruby コードを無理やり解析する静的型解析器。RBSを出力する。LSP対応のLanguage Serverとしても動作する。TypeProf for IDEというVSコード拡張を使うと、補完、コードジャンプ、型不一致のエラー出力などができる。解析があまり速くない。
https://techlife.cookpad.com/entry/2020/12/09/120314
https://github.com/soutaro/steep
Sorbet
Rubyの静的型検査器。ソースコード内に型を記述するか、RBIと呼ばれる独自の型記述言語を使う。プログラマが型を記述することを前提としていて、記述した部分について高速に解析できる。LSP対応のLanguage Serverとしても動作する。VSコード拡張がある。
Tapioca
RBIを自動で生成してくれるツール。
https://github.com/Shopify/tapioca
トーク
- https://rubykaigi.org/2023/presentations/egiurleo.html#may11
- https://rubykaigi.org/2023/presentations/tompng.html#may11
- https://rubykaigi.org/2023/presentations/mametter.html#may12
- https://rubykaigi.org/2023/presentations/Morriar.html#may13
- https://rubykaigi.org/2023/presentations/p_ck_.html#may13
過去のトーク
- https://rubykaigi.org/2022/presentations/oceanicpanda.html#sep08
- https://rubykaigi.org/2022/presentations/ksss.html#sep09
- https://rubykaigi.org/2022/presentations/pink_bangbi.html#sep10
- https://rubykaigi.org/2022/presentations/soutaro.html#sep10
- https://rubykaigi.org/2021-takeout/presentations/mametter.html
- https://rubykaigi.org/2021-takeout/presentations/p_ck_.html
WASM対応
WASM(Webassembly)
スタックベースVM向けの命令フォーマット。凄く雑に言うと、Webブラウザ用のYARV命令列のようなもの。ブラウザで動かすことを想定しているが、環境に依存しない命令セットとしてエッジコンピューティングなどでも使われ始めている。
WASI
WASMさえあればRubyが行う全ての操作を実現できるわけではない。WASMでは、システムのリソース(時計、ストレージなど)を操作する方法は定義されいない。
WASIは、WASMからシステムのリソースへのインターフェースを定義する仕様。WASMとWASIを実装している環境であれば、WASMからシステムのリソースを操作できる。
WASMでのRubyプログラム実行の方法
RubyをWASMで動かす際は、2つのリソースを使う。
ブラウザでWASMで書かれたRubyインタプリタを動かして、そこにRubyスクリプトを入力として与える。
- Rubyスクリプトを別途配布するのは面倒なので、wasm-vfsを使ってこれらのファイルをひとまとめにできる
自分で書いたスクリプトをwasm化する方法
https://zenn.dev/koduki/articles/3619f53e8c0575
トーク
- https://rubykaigi.org/2023/presentations/aaaa777.html#may11
- https://rubykaigi.org/2023/presentations/ledsun.html#may13
過去のトーク
おわりに
Rubyを支える技術について、広く浅く紹介させていただきました。RubyKaigiの発表では興味深い話がたくさん聞けるので、より楽しむための一助になれば幸いです。
宣伝
SmartHRではソフトウェアエンジニアを募集しています!SmartHRに少しでも興味をお持ちの方は、ぜひご応募ください!
- まだ応募するまでは至らないけど、ちょっと話を聞いてみたい
- SmartHRの開発者とキャリアや技術的な話をしたい
- 松本のおすすめグルメ情報を知りたい
そんな方は、RubyKaigiにSmartHRのエンジニアも参加しているので、よければ一緒にお話しましょう!
付録
MJITで生成されるCのコードを確認する
ruby --dump=insns -e "def three = 1+2; three; three" == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,29)> (catch: false) 0000 definemethod :three, three ( 1)[Li] 0003 putself 0004 opt_send_without_block <calldata!mid:three, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0006 pop 0007 putself 0008 opt_send_without_block <calldata!mid:three, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0010 leave == disasm: #<ISeq:three@-e:1 (1,0)-(1,15)> (catch: false) 0000 putobject_INT2FIX_1_ ( 1)[Ca] 0001 putobject 2 0003 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] 0005 leave [Re] ruby --mjit-verbose=1 --mjit-call-threshold=2 --mjit-save-temps --mjit-wait --disable-gems -e "def three = 1+2; three; three" JIT success: three@-e:1 JIT batch (135.7ms): Batched 1 methods /var/folders/n2/_m267r4d4sl2z0yw8mnr0yhh0000gq/T//_ruby_mjit_p49806u1.c -> /var/folders/n2/_m267r4d4sl2z0yw8mnr0yhh0000gq/T//_ruby_mjit_p49806u1.bundle Successful MJIT finish cat /var/folders/n2/_m267r4d4sl2z0yw8mnr0yhh0000gq/T//_ruby_mjit_p49806u1.c /* three@-e:1 */ VALUE _mjit0(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp) { VALUE stack[2]; static const rb_iseq_t *original_iseq = (const rb_iseq_t *)0x1053a5ba0; static const VALUE *const original_body_iseq = (VALUE *)0x6000019d4000; VALUE cfp_self = reg_cfp->self; #undef GET_SELF #define GET_SELF() cfp_self label_0: /* putobject_INT2FIX_1_ */ { MAYBE_UNUSED(unsigned int) stack_size = 0; MAYBE_UNUSED(VALUE) val; val = INT2FIX(1); { /* */ } stack[0] = val; } label_1: /* putobject */ { MAYBE_UNUSED(unsigned int) stack_size = 1; MAYBE_UNUSED(VALUE) val; val = (VALUE)5; { /* */ } stack[1] = val; } label_3: /* opt_plus */ { MAYBE_UNUSED(unsigned int) stack_size = 2; MAYBE_UNUSED(CALL_DATA) cd; MAYBE_UNUSED(VALUE) obj, recv, val; cd = (CALL_DATA)105553139318832; recv = stack[0]; obj = stack[1]; { val = vm_opt_plus(recv, obj); if (val == Qundef) { reg_cfp->sp = vm_base_ptr(reg_cfp) + 2; reg_cfp->pc = original_body_iseq + 3; goto cancel; } } stack[0] = val; } label_5: /* leave */ if (UNLIKELY(RUBY_VM_INTERRUPTED_ANY(ec))) { reg_cfp->sp = vm_base_ptr(reg_cfp) + 1; reg_cfp->pc = original_body_iseq + 5; rb_threadptr_execute_interrupts(rb_ec_thread_ptr(ec), 0); } ec->cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(reg_cfp); return stack[0]; send_cancel: rb_mjit_recompile_send(original_iseq); goto cancel; ivar_cancel: rb_mjit_recompile_ivar(original_iseq); goto cancel; exivar_cancel: rb_mjit_recompile_exivar(original_iseq); goto cancel; const_cancel: rb_mjit_recompile_const(original_iseq); goto cancel; cancel: *(vm_base_ptr(reg_cfp) + 0) = stack[0]; *(vm_base_ptr(reg_cfp) + 1) = stack[1]; return Qundef; #undef GET_SELF } // end of _mjit0