Skip to content

Instantly share code, notes, and snippets.

@osyoyu
Last active March 20, 2024 08:56
Show Gist options
  • Save osyoyu/0bda2d02fd073ebe0240e6414441c6b4 to your computer and use it in GitHub Desktop.
Save osyoyu/0bda2d02fd073ebe0240e6414441c6b4 to your computer and use it in GitHub Desktop.
Ruby のパフォーマンスプロファイリングの改善 (Ruby Association Grant 2023 最終報告)

Ruby のための新しいプロファイラ Pf2 の開発 (Ruby Association Grant 2023 最終報告)

Daisuke Aritomo (osyoyu)

新たな Ruby プロファイラ "Pf2" を開発し、Gem としてリリースした。既存のプロファイラにはない、以下のような機能を備えたプロファイラを目指し、現在も開発を継続している。

  • プロファイリングの対象とする Ruby Thread を制御できる
  • 実時刻 (wall time) ベースのプロファイルの場合、あるサンプル時刻において複数の Ruby Thread のサンプルを収集できる
    • 既存のプロファイラでは1つの Thread のみのサンプルを収集する
  • サンプリングのタイミングをより精緻に制御できる
    • 既存のプロファイラでは一定の (a) wall time (b) プロセスが消費した CPU 時間 が経過したタイミングの2つのみを指定できる
    • Pf2 では加えて、一定の (c) Ruby Thread が消費した CPU 時間 が経過したタイミングを指定できる
  • GVL のコンテンションを確認できる
  • 任意のタイミングを記録できるマーカー機能

以下に Pf2 の特徴や評価を述べる。

Firefox Profiler によるビジュアライズ

Web ブラウザの Firefox 用のプロファイラである Firefox Profiler のビジュアライザが解釈できる形式の出力を生成できるようにした。Firefox Profiler は call-tree view, inverted call-tree view, flamegraph など多彩なビジュアライズを提供しており、パフォーマンス改善の方法を複数の切り口から検討するにあたって非常に有用である。

Ruby スタックと native (C-level) スタックの統合

Pf2 がサンプルを採取する際、Ruby のスタックと native (C-level) スタックの両方を保存し、統合された状態で表示する機能を実装した。

この機能が実用的となるシーンの例を以下に挙げる。

(例1) Native extension (C 拡張) 内の実装のプロファイリング

プロファイラはプログラムの隅々までを正確に計測することが求められ、ライブラリや Gem の中身もその例外ではない。しかし、native extension (C 拡張) を伴う Gem については、Ruby のメソッド単位のプロファイリングでは不十分である。

そのような Gem の一例として、mysql2 が挙げられる。 mysql2 が提供する Mysql2::Client#query メソッドを利用することで、簡便に MySQL サーバーにクエリすることができる。このメソッド内では (1) MySQL サーバーへのクエリの送信・結果の待機 (2) 結果のパース (3) 結果の Ruby オブジェクトへの変換 などの処理が行われているが、これらは C 関数内で実装されているため、既存の Ruby プロファイラではこれらを峻別することができない。

client = Mysql2::Client.new
client.query("SELECT * FROM ...")

Pf2 では Mysql2::Client#query の実装である C のレベルに立ち入り、プロファイルを採取し可視化する機能を備えているので、クエリ結果の待機に要する時間と結果のパースに要する時間とを識別することができる。

(例2) Ruby スタックに出現しない Ruby メソッドの評価

現在の CRuby 実装では、プロファイラ実装の基盤である rb_profile_thread_frames() 関数でキャプチャすることができないメソッドがいくつか存在する。一例として Hash#[]Array#<< がこれに該当する。

Pf2 の native stack 統合機能を活用することで、この問題を緩和することができる。以下はこのプログラムを Pf2 でプロファイリングした例である。Flamegraph 中に Hash#[] ($res[key] の部分) は出現していないものの、それらの実装に対応する rb_hash_aref() を見てとることができる。

$res = {}
def parse_and_store(str)
  key, value = str.split('=')
  $res[key] ||= []
  $res[key] << value
  # この例では以下のような最適化が可能
  # target = ($res[key] ||= [])
  # target << value
end

while line = gets
  parse_and_store(line.chomp)
end

なお、Hash#[]Array#<< が Ruby レベルで直接観察できないのは、専用の YARV 命令(それぞれ opt_aref, pushtoarray)が割り当てられており、スタックフレームを積まない形で YARV バイトコードに変換されることが理由である。他にも同様のメソッドは複数存在する。

Rust による実装

Pf2 は native extension を伴う Gem として実装した。ネイティブ部分の実装では C が利用されることが多いが、Pf2 では Rust を選択した。標準ライブラリが備える充実したデータ構造や、扱いやすい mutex 実装が存在することが主な理由である。

なお、Rust はメモリ安全性を備えた言語として知られているが、プロファイラの性質上 CRuby API や libc の呼び出しが多く、必然的に unsafe を多用したため、この部分における効果は限定的であった。

今後の展望

Pf2 のプロファイリングの基礎となる計測関連の実装は一定の完成度に達しているものの、ターゲットプログラムへの組み込みやすさなどのユーザビリティには課題が残る。実世界の Ruby プログラムをプロファイリングするときの第一選択を目指し、今後も開発を継続する。

また、今回の成果を RubyKaigi 2024 にて発表するとともに、普及活動としてブログや技術記事の公開を進める。

謝辞

本プロジェクトの期間を通じ、メンターの国分さんには CRuby の構造やプロファイラに求められる機能など、多くのアドバイス・サポートをいただきました。笹田さんや遠藤さんにも異なった視点からの助言をいただきました。この場を借りて感謝申し上げます。また、このプロジェクトに集中して取り組む機会をいただきました Ruby Association にも感謝いたします。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment