Rubyコミッター・笹田耕一に世代別インクリメンタルGCを発想したプロセスを聞いてみた
Rubyのフルタイムコミッターである笹田耕一さんに、Rubyの処理性能を向上させるいくつかのブレイクスルーをどのように解決し、どのような困難があったのかを聞きました。
直感的な文法や生産性の高さから、世界中の人々に愛されるオブジェクト指向スクリプト言語Ruby。その黎明期から現在に至るまで、大きな変化を遂げてきた要素があります。“処理速度”です。数々の最適化が行われた結果、Rubyの処理性能はかつてとは比べものにならないほど向上しました。
その改善を支えたのは、世界中のRubyコミッターたち。中でも、性能向上において多くの成果を残してきたのが、クックパッド株式会社でフルタイムRubyコミッターとして働く笹田耕一(ささだ・こういち/
- 「実行時にしか分からない情報」があるからこそ、難しい
- 「GCが遅い」という課題をどう解決したか?
- 「Procオブジェクト生成の必要がないこと」を判定するために
- 非互換の変更を入れる場合は、“特典”も一緒に盛り込む
- 考え続けること。そして、手を動かすこと
- 私のライフイベントは、Rubyに支えられている
「実行時にしか分からない情報」があるからこそ、難しい
──Rubyとは、どのような特性を持つ言語なのでしょうか?
笹田 スクリプト言語であること、オブジェクト指向であることなど、さまざまな特徴がありますが、性能に大きく影響するのは「実行時にしか分からない情報がたくさんあること」です。
笹田 例えば、Javaなどの言語は型の記述があるため、実行前の段階で「このメソッドの引数には必ず○○型が入ってくる」と分かることが多いのです。また、処理の中でどんなメソッドが呼ばれるかについても、ある程度は特定できます。
一方で、Rubyはそれがあまりできません。だからRubyの開発を行う際には、「実行途中でどんな情報が手に入れば、後続の処理を最適化できるか」を考えた上で設計を行う必要があります。逆に言えば、その最適化を適切に行うことが、処理の高速化に直結するわけです。
とはいえ、「なんでもかんでも最適化して高速化すればいい」というわけではありません。最適化のためのプランは何案もあるわけですが、その中には「○○を入れれば速くなるのは確実だけど、この施策をやってしまうとその後のメンテナンスがめちゃくちゃ大変になる」ものもあるわけですね。
Rubyのコントリビューターの人数や開発に割ける工数は限られていますから、なんでもやるわけにはいかない。“コスパ”の良いプランを選んでいかなければならないわけです。なるべくメンテナンスがしやすく、かつ効果のある施策を選ぶようにしています。
「GCが遅い」という課題をどう解決したか?
笹田 例えば、かつてRubyには「ガベージコレクション(以下、GC)の処理が遅い」という課題がありました。昔から、世代別GCというテクニックを導入することで、この課題を解決できると分かっていたのですが、それを導入するためには後方互換性を切らなければいけないと考えられていました。
──世代別GCとはどのようなものでしょう?
笹田 世代別GCとは、「プログラムにおいては新しいオブジェクトの方が(古いオブジェクトよりも)すぐに死ぬ可能性が高い」という「世代別仮説」に基づいたGCのしくみです。
笹田 「古いオブジェクトを格納する世界」と「新しいオブジェクトを格納する世界」を別々に用意して、後者の世界だけを頻繁にGCしてやることで、効率が良くなります。メモリが足りなくなってきたら、古い世界と新しい世界を全部まとめてGCしてやります。
手法としては1970年代くらいからあり、GCの機能を持ったプログラミング言語ではよく用いられるテクニックです。
──この手法を用いるために、後方互換性を切る必要があると思われていたのはなぜでしょうか?
笹田 細かい話になりますが、RubyではインタープリターにCで書かれたプログラムを読み込ませて使えます。世代別GCを入れた場合、Cで書かれたインタープリターや拡張ライブラリで問題が起こることが分かっていました。
世代別GCでは、新しいオブジェクトを集めた空間だけを確認しても、古い世代から新しい世代に対してポインタの参照があるかが分からないため、不要と判断されてGCされてしまう問題があります。
古い世代から新しい世代への参照を検知するには、ライトバリアという仕組みが必要になります。つまり、「C言語の拡張モジュールに対しては、ライトバリアを全て入れてもらう」ことが、我々の想定していた非互換の変更でした。
しかし、この修正を強制するのはとても難しいので、あまり現実的ではありませんでした。
──かなり影響が大きそうですね……。
笹田 はい。しかし、私はある時、「それぞれのオブジェクトに対して、ライトバリアを入れたか入れていないかのマークをあらかじめ付けておくことで、誤ってオブジェクトが消されることを防止し、世代別GCがうまく動く」ことを発見したんです。これで一気に道が開けました。
世代別GCは、2013年12月リリースのRuby 2.1から導入され、GCがかなり速くなりました。

「Procオブジェクト生成の必要がないこと」を判定するために
──笹田さんは、Ruby 2.5で「Lazy Proc allocationによるブロックパラメータを用いたブロック渡しの高速化」にも携わっています。この施策についても解説していただけますか?
笹田 この施策の概要は、ブロックパラメータを使ってブロックを受け渡すと、毎回Proc
オブジェクトを作るのはパフォーマンスが非常に悪いため、Proc
オブジェクトを作らずにブロックの情報だけを素直に渡す仕組みに変えたというものです。
実は、私が作ったVMの一番のウリは、Proc
オブジェクトを作らずに、メソッドにブロックを渡せるので、そこが高速であるというものでした。この特長を、ブロックパラメータを用いた場合でも実現した、というものです。
── Proc
オブジェクトを生成すると処理が重くなってしまうのはなぜですか?
笹田 Proc
オブジェクトは、そのProc
を後からコールできますよね。そして、コール時にはオブジェクトが持つローカル変数にもアクセス可能なんです。だからローカル変数の寿命もProc
オブジェクトと同じにするため、全てのローカル変数のスコープをコピーする処理が必要になります。
それが1個のProc
オブジェクトだけならまだましなんですが、数珠つなぎでメソッドフレームをつなぐケースがあって。そうなると連鎖的にどんどんオブジェクトが生まれていきます。
──確かに重そうな処理ですね。
笹田 そのため、処理を最適化するには「なるべくProc
オブジェクトを作らないよう、必要になるまでProc
オブジェクトの生成を遅延させる」ことが重要になってきます。これを「Lazy Proc allocation」と呼びます。
Lazy Proc allocationを実現するには、「Proc
オブジェクトが必要か、不要か?」を何かの方法で判断する必要があります。この高速化のための手法はだいぶ長い間検討していたんですが、判定のためにはエスケープ解析という機構を相当作り込まなければいけないと思っていました。かなり難易度が高いだろうと。

以下は、笹田さんが「クックパッド開発者ブログ」に掲載した「Lazy Proc allocation」の解説を一部抜粋・編集したもの。
Ruby 2.5 の改善を自慢したい - クックパッド開発者ブログ
def sample1 &b block_yield(&b) end
このプログラムは、b
をProc
にする必要はない。ブロックの情報のまま、他のメソッドに渡してやればいいため。
def sample2 &b b end
このプログラムは、b
をProc
にする必要がある。呼び出し側が返値としてProc
オブジェクトを期待する可能性があるため。
def sample3 &b foo(b) end
このプログラムも、b
をProc
にする必要がある。foo
を呼んだ先でProc
オブジェクトを期待する可能性があるため。
block_yield(&b)
のようにしか使っていなければ、b
はブロック情報のままで良さそうに見える。しかし、以下の例でそうではないことが分かる。
def sample4 &b get_b(binding) end
一見すると、b
は触っていないため、ブロック情報のままで良さそうに見える。だが、binding
オブジェクトを用いると、そのバインディングを生成した箇所のローカル変数にアクセス可能であるため、get_b
の定義を
def get_b bind bind.local_variable_get(:b) end
のようにすると、b
の中身にアクセスできる。この場合、b
がsample4
の返値になるためProc
オブジェクトする必要性が生じる。binding
が出現したら諦めるという方法もあるが、binding
はメソッドであるため、任意の名前にエイリアスをつけることが可能だ。
つまり、どんなメソッド呼び出しもbinding
になる可能性があるため、プログラムの字面を見て「b
をProc
オブジェクトにする必要はないと言い切るのは難しい」と笹田さんはかつて判断していたそうだ。
笹田 いろいろと考えましたが、ある日自転車に乗っている最中に「ブロックパラメーターへのアクセスだけを特別に実行時に監視すれば、Proc
オブジェクトの要否を判別できる」ことに気づきました。そこから実現につながりました。
非互換の変更を入れる場合は、“特典”も一緒に盛り込む
続きをお読みいただけます(無料)。

- すべての過去記事を読める
- 過去のウェビナー動画を
視聴できる - 企業やエージェントから
スカウトが届く