SmartHR Tech Blog

SmartHR 開発者ブログ

RubyKaigi 2023に向けた勉強会を行いました

こんにちは。プロダクトエンジニアのyudaiです。5/11から開催されるRubyKaigi2023に向けて、社内で勉強会を実施しました。自己学習のきっかけづくりとして、RubyKaigiのトークを聞く準備方法、トークテーマ、RubyKaigiで話題になりがちな技術について広く浅くお話しました。

RubyKaigi 2023に参加される方のお役に立つかもしれませんので、この勉強会で使った資料を公開します。

RubyKaigi 2023 予習資料

留意事項

この記事はyudai(@ytnk531)が、RubyKaigi 2023の予習のために調べた内容をまとめたものです。誤った内容や古い内容が多く含まれる可能性があることをご留意ください。誤りなどについてご指摘を頂ける場合は、TwitterのDMかリプライなどでご連絡をいただけると大変助かります。

内容

  • 事前に理解していたほうが良さそうなRubyの仕組み(パーサ、JIT、並行処理あたり)の紹介
  • ここ数年でRubyKaigiで話題になっている内容の紹介

対象

  • RubyKaigiに初めて参加する方
  • RubyKaigiでいきなりセッションを聞いて理解できるか不安な方

RubyKaigiの準備

RubyKaigiの話題の分類

  1. Rubyの未来の話(今後の展望、新文法、新機能)
  2. Rubyの新機能の詳しい話(実装の詳細、設計、最適化など)(WebAssembly、JIT、型解析…)
  3. 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

トーク

CRubyのプログラム実行の流れ

Rubyプログラムは、下記の工程を経てYARV命令列に変換され、YARVで実行される。

  • Rubyプログラムを字句解析してトークン列を得る
  • トークン列を構文解析してASTノードを得る
  • ASTノードをコンパイルしてYARV命令列を得る

Rubyプログラム実行の概略 引用元: https://patshaughnessy.net/2012/6/18/the-start-of-a-long-journey-how-ruby-parses-and-compiles-your-code

YARV

YetAnotherRubyVM単にRuby VMとも。Rubyのためのスタックマシン型の仮想マシン。YARV命令列を一つずつ処理する。実際のプログラム実行はこのプログラムで行われる。

https://ja.wikipedia.org/wiki/スタックマシン

Rubyプログラム

10.times do |n|
  puts n
end

AST(Absyract Syntax Tree, 抽象構文木)ノード

簡単なプログラムのAST

YARV命令列(インストラクションシークエンス, insns, バイトコードとも)

簡単なプログラムのYARV命令列

詳しくはRubyのしくみを参照。

トーク

過去のトーク

JITコンパイラ

Just In Time Compiler。YARV命令列をYARVに順番に与えるのではなく、最適化されたネイティブコード(機械語)を動的に生成することで高速化する。ネイティブコードにするから速いというよりは、実行時の情報を用いて最適化できるので速くなる。いくつか種類があって、有名なものだとMJIT、YJITがある。

JITコンパイラの概略 引用元: https://speakerdeck.com/k0kubun/rubykaigi-2022?slide=9

YJIT

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

トーク

過去のトーク

(Object) Shape

Rubyのオブジェクトは、インスタンス変数へのアクセスに時間がかかる。TruffleRubyでは、Object Shapeという技術を使ってこの問題を改善した。CRubyにもShapeを実装しようという流れがある。

トーク

今年はなさそうだがYJITの性能向上にShapeが一役買ったようなので、YJIT関連の発表で触れられるかも。

過去のトーク

並列処理

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=がちである。-,カーネルレベル・スレッドとユーザレベル・スレッド,-スレッドの実装

トーク

過去のトーク

メモリ管理

ガベージコレクション(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

トーク

過去のトーク

静的型付け

ここ数年、静的型付け言語が隆盛を極めている影響か、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コード拡張がある。

https://sorbet.org/

Tapioca

RBIを自動で生成してくれるツール。

https://github.com/Shopify/tapioca

トーク

過去のトーク

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

トーク

過去のトーク

おわりに

Rubyを支える技術について、広く浅く紹介させていただきました。RubyKaigiの発表では興味深い話がたくさん聞けるので、より楽しむための一助になれば幸いです。

宣伝

SmartHRではソフトウェアエンジニアを募集しています!SmartHRに少しでも興味をお持ちの方は、ぜひご応募ください!

hello-world.smarthr.co.jp

  • まだ応募するまでは至らないけど、ちょっと話を聞いてみたい
  • 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