こんにちは、エンジニアの toki (@tokai235) です。法人向け eGift サービス giftee for Business の開発をしています。普段はバックエンドやインフラのお仕事が多いですが、フロントエンドが好きです(?)。
2025/04/16 - 2025/04/18 で開催された、RubyKaigi 2025@松山 に参加してきました。鯛めしとお酒がうまかったです。最高です。
とはいえ飲んだくれてるばっかりではなく、セッションを聞いたり企業ブースを回ったりしていました。
その中でも Ruby Committer の Aaron Patterson 氏(@tenderlove) のトーク 「Speeding up Class#new」 が非常に聞き応えがあったので、書き留めておこうと思います。またせっかくなので、その際に Ruby のソースを見ながら理解を深めていこうと思います。
なお Ruby のソースコードは ruby/ruby リポジトリに公開されています。
セッションの内容
このセッションの内容は、Aaron 氏が実装しているこの Pull Request #9289 の実装を解説したものになります。なのでこちらを見ながら読み進めると理解の助けになると思います。
またAaron 氏の Speaker Deck に以下の資料が公開されています。RubyKaigi 当日の資料とは構成が違い、主にベンチマーク結果をまとめたもののようですが、改善効果を知りたい方は見るといいでしょう。
1. Class#new の現実装について
セッションではまず Class#new の現在の実装を見ながらなぜ遅いのかについて解説がありました。
- オブジェクトを allocate する
- initialize する
- オブジェクトを return する
これらの処理は現状 C で実装されているそうです。そのため、Class#new を実行すると、Ruby => C => Ruby 言語を行き来することになり、これがボトルネックになっていると Aaron 氏は言います。
これを実際の Ruby ソースで確認してみます。
Ruby 3.4 Docs の class Class を見てみると、Instance Method new
があることがわかります。(余談ですが、ちなみに匿名クラスを作るための Class Method new
というのもあるようです。) このドキュメントにソースコードの記載があり、内部では rb_class_new_instance_pass_kw
という関数が呼ばれているようです。Ruby のソースコードを見に行くと、rb_class_new_instance_pass_kw
は ruby/object.c
に実装されています。ソースコードの該当箇所
VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; }
これを見るとたしかに allocate, initialize, return をやっているように見えます。
そして rb_class_new_instance_pass_kw
は object.c 内で rb_define_method を使って rb_class_new_instance_pass_kw
を class Class の new
メソッドとして定義しているように見えます。
rb_define_method(rb_cClass, "new", rb_class_new_instance_pass_kw, -1);
つまり、Class#new は Ruby から呼び出されると、C で実行され、Ruby にオブジェクトを返す、ということがソースコードからも読み解けました。
2. なぜ C と Ruby を行き来すると遅いのか
異なる言語を1つのプログラム内で行き来すると処理が遅い、というのはなんとなくイメージ通りですが、実際のところなぜ遅いのでしょう?
これは Ruby と C の呼び出し規約が異なり、変換が必要なためであると Aaron 氏は言っています。ここで言う呼び出し規約とは、変数とメソッドの位置を探索するルールのことです。C と Ruby では変数などのさまざまなデータの扱いが違います。例えば Ruby ではキーワード引数を使うことができますが、C ではサポートされていません。なので変換が必要ということです。
doc/extension.ja.rdoc に「Rubyの拡張ライブラリの作り方」のドキュメントがあり、詳細についてはここに記載されています。
3. Class#new を高速化する
では本題の Class#new の高速化です。
Class#new 内外の Ruby と C の変換がボトルネックになっていたため、アプローチとして Class#new も Ruby で実行すればこの変換の処理がなくなり、速くなるのではないかと Aaron 氏は予想しました。そこで Class#new の Ruby 実装を追加したとのことです。
セッションでは実装におけるポイントについていくつか紹介されていました。
initialize の扱い
素朴に Class#new を Ruby 実装すると例外が発生してしまいます。これは initialize が private method であり、かつ Class オブジェクトのもとになる BasicObject に send
メソッドがないことによります。
そこで Aaron 氏は新たに send_delegate!
という新たな Primitive (Ruby 内で呼び出せる C function)を実装し、この問題を解消したそうです。
例外処理
send_delegate!
を使うようになったことで、例外が発生するようになりましたが、プリミティブな処理で例外が発生すると、Ruby の実行などにもかなり大きな影響があります。そこで新たに rb_class_alloc2
という新たな Primitive を実装し、FCALL フラグで実行することで、例外なしで安全に実行できるようにしたとのことです。
4. ベンチマークテスト
この変更で実際にどの程度パフォーマンスが改善されたのかをテストされています。以下にざっくりテストの内容についてまとめました。
- 指標としては allocations per second を測った
- いくつかの観点で比較をした
- 引数の数
- キーワード引数の有無
- インラインキャッシュの有無
結果としては、1つの位置引数では 1.8倍の高速化、キーワード引数では最低でも 1.4倍の高速化が実現できたそうです。またキーワード引数の数が多いほど効果は顕著で、10キーワード引数での効果は 6.4倍にもなるとのことです。
5. デメリットについて
この変更のデメリットについても述べられていました。大きくは以下2点です。
- メモリの増加
- ただしこれはコード全体の中ではごく僅かなものとのこと
- スタックトレースの出力の変更
- 例外が発生した場合にエラー箇所の特定のために出力されるスタックトレースにおいて、これまでは出力されていた Class#new のトレース行が出力されなくなるそう
- Aaron 氏はこれを許容できるデメリットであると述べています
まとめ
この記事では RubyKaigi 2025 のトークセッションの1つ、「Speeding up Class#new」を Ruby のソースコードも見ながら深ぼっていく、ということをしてみました。
セッションの内容自体も非常に興味深いものでしたが、Ruby のソースを読み解いてみることで、普段自分たちのプロダクトコードを開発しているときのように Ruby のことを理解していくことができ、より Ruby を身近に感じることができるようになりました。
ギフティでは Ruby 読めるぜ!という方も Ruby 読みたいぜ!という方も大募集しています。興味がある方はぜひカジュアル面談などでお話しましょう!