こんにちは、ソフトウェアエンジニアの千葉です。自分は RubyKaigi で Ruby コミッターそれぞれから今後の熱い展望を聞き、興奮冷めやらぬところですが、皆様はイカがお過ごしでしょうか。
今回、日本最大の Ruby に関するカンファレンスである RubyKaigi に Wantedly がスポンサードし、いくつかの講演を聴講させていただいています。(Wantedly はスポンサーブースを設けていて、エンジニアが執筆した技術書の配布等を行っています!!)
RubyKaigi の2日目、たくさんの Ruby (と C) の話を聞いて心踊っているのですが、ここでは Ruby の最適化に関する発表を2つ、紹介させていただきます。
Method-based JIT compilation by transpiling to Julia
1つは、Kenta Murata さんによる 「Method-based JIT compilation by transpiling to Julia」です。
この発表は、JIT を用いた高速化についてなのですが、 (MJIT や YJIT とは異なる) 独自の JIT を実装し、対象のコードに Ruby の動的機能の一部を使用しないという制約のもと、大きな高速化を実現する、というものでした。
このアプローチのモチベーションとなったのが、Ruby で大規模な数値計算で行いたいというものでした。 Ruby 上で数値計算を実現するために、大規模なデータを扱うための (https://red-data-tools.github.io/ らによる) ライブラリの整備が進んでいたのですが、実現のボトルネックとなっ ていたのが、Ruby の一部の動的機能でした。
その例として、Ruby ではメソッドを動的に再定義することが行える、ということがあります。これ自体は便利に利用できる場面もあるのですが、動的に行えるためメソッドを実行するたびに再定義が行われているかのチェックを行う必要があるのですが、数値計算の場合、こうした機能を利用することは少ないし、チェックのためのオーバーヘッドがパフォーマンスに悪影響を与えていました。(もちろん C extension として実装すれば起きない問題ではありますが、 Ruby で数値計算処理を行いたいというモチベーションからは外れています。)
また、MJIT や YJIT は基本的に Ruby の仕様に忠実に動作するため、この問題に対応出来ていないとのことでした。
こうした問題を解決するために取ったアプローチというのが、「こうした動的機能を使わず、高速に処理したいコードを、 JIT により実行時に動的機能のチェックを行わない機械語にコンパイルし、実行する」というものでした。
このアプローチは numba という Python 用の JIT コンパイラで取られている手法です。
numba は (以下のコードのように) decorator で指定した function に対して、Python の一部の機能が使えないという制約の代わりに、numba が最適化を行います。この手法を Ruby に持っていこうというのが今回のアイデアでした。
from numba import njit
import random
@njit
def monte_carlo_pi(nsamples):
acc = 0
for i in range(nsamples):
x = random.random()
y = random.random()
if (x ** 2 + y ** 2) < 1.0:
acc += 1
return 4.0 * acc / nsamples
(https://numba.pydata.org/ より)
JIT コンパイルするには、 Ruby の AST または YARV のバイトコードを最適化しつつコンパイルするのをを行う必要があるのですが、これをフルスクラッチするのではなく、「難しいことは Julia にやらせよう」ということで、
Ruby の AST → Julia のコード → 機械語
という工程で、Ruby の AST から Julia のコードへの変換 (と言語間の差分の吸収) を行い、最適化及びコンパイルは Julia に任せる、というアプローチが取られていました。こうした設計で実装を行い、いくつかの数値計算プログラムでベンチマークを行ったところ、大幅な高速化が見られたとのことでした。
今回の実装の設計について解説している (と思われる) 内容は、今回の発表を行った Kenta Murata さんが以下のブログでまとめています。
RubyKaigi 1日目の k0kubun さんの発表で、「Ruby 3.2 で MJIT が Ruby 実装化し、モンキーパッチできるようになった」という話がありましたが、こうした、仕様に制約をもたせ、特定の分野に特化するような独自の JIT を実装してみる、というのも価値があり、非常に面白いかもしれません。
Implementing Object Shapes in CRuby
もう1つは Jemma Issroff さんの 「Implementing Object Shapes in CRuby」 です。これは CRuby に対し、現在導入が提案されている Object Shapes を用いた改善についての解説でした。
ここで提案されている Object Shape とは、「Ruby オブジェクトの形」を表現するデータ構造のことです。オブジェクトの形を表す情報として、「オブジェクトの持つインスタンス変数それぞれの名前」「freeze されているか」などの情報を持ちます。
これにより様々な恩恵があり、例えばインスタンス変数の取得をする際のキャッシュとすることで、これまでのクラス毎のキャッシュよりもヒットしやすくなり高速化になったり、様々なチェックの処理などをシンプルにしやすくできるそうです。実際に、一部のベンチマーク (継承したクラスインスタンスに対するインスタンス変数アクセス) では2倍程度の高速化になったそうです。
今回の発表に関連した Feature Request は以下で今回の改善について解説があり、 GitHub 上に Draft 実装もあります。
Object Shape については、去年の Rubykaigi でも TruffleRuby における最適化手法という形で紹介されています。この手法に関する研究、アイデアのルーツとして、プログラミング言語の Smalltalk や Self から始まったということも解説されています。
今回紹介した内容はどちらも、他言語で過去に実用や研究されていた手法を、うまく利用して Ruby に落とし込んでいて、聞いていて非常にエンジニアとして心揺さぶられます…!
ちなみに、こうした改善の話など、RubyKaigi の発表を聞いていると、 Ruby の内部データ構造の話がめちゃくちゃ出ますが、Ruby のオブジェクトがどういうデータ構造になっているか、インスタンス変数はどう持っているかなどは、Rubyのしくみ Ruby Under a Microscope という書籍が参考になります。
(発行されてからしばらく経つので古い内容もありますが)
RubyKaigi も残り1日、最終日も面白そうな発表が目白押しで楽しみですね!Wantedlyから参加したエンジニアが他の記事をまだまだ出していきます。そちらも御覧ください!