こんにちは、株式会社Techouse バックエンドエンジニアの @nodematerial です。 今回は、RubyKaigi 2024 1日目のセッション「The depths of profiling Ruby」を聴講したので、その内容をブログにまとめさせていただきます。
プロファイラとは
プロファイラはコードのパフォーマンスを調べるためのツール全般を指します。 プロファイラには、コードの中で最も遅いところ(ホットスポット)を特定するという大きな役割があります。
Pf2 について
スピーカーの osyoyu さんは Pf2 というプロファイラを作成されており、発表ではPf2の実装を中心に解説が行われました。
Pf2 に特徴的な機能
マルチスレッドのプロファイリング
GCの所要時間や、GVL によって有効活用できなかった時間を表示することができ、スレッド毎の実行状況の統計を取ることができます。
C 拡張呼び出しのプロファイリング
一般的な Ruby プロファイラは Ruby のスタックトレースのみ取得できますが、Pf2 は加えて C 拡張(C extension)の呼び出しも追跡することができます。
C 拡張呼び出しを追跡できることで、Ruby レベルのプロファイラでは見えないボトルネックを特定できる可能性があります。
一例として、SQL 送信処理命令 do_send_query
のプロファイルによる、ネットワーク部分のボトルネック特定の可能性について紹介されていました。
隠れた Ruby メソッドを調べる
プリミティブなRuby オブジェクトに対するメソッド呼び出しは、C 拡張の中で実行されることがあるため、Ruby レベルのプロファイルでは調べられない場合もあります。 発表で紹介されていた例では、Hash#[]
でボトルネックが発生していました。Hash#[]
は CRuby による最適化 (optimization) によって、Ruby スタックから隠されて見えなくなるため、通常のRubyプロファイラで原因箇所を特定するのは難しいですが、Pf2を使うことでこれが可能になることが、発表で紹介されていました。
Pf2 で使用できる可視化手法
- フレームグラフ(flame graph)
- 呼び出しツリー(call tree)
- 逆呼び出しツリー (inverted call tree)
逆呼び出しツリーは、 通常の呼び出しツリーとは逆に、特定のメソッドの「呼び出し元」を調べることができます。これによって、 数多く呼び出されているメソッドを特定することができます。
次に、実行情報の取得, 取得した情報の展開と集約, 可視化の3ステップで Pf2 の実装解説が行われました。
1. 実行情報の取得
プロファイラに求められる性質
プロファイラには、以下2つの性質が求められます。
また、両者の性質はトレードオフの関係にあることが、発表の後半で説明されます。
- 可能な限り informable(たくさんの情報を提供できる状態) であること
- そのために、できるだけたくさんの情報を収集する
- プロファイラ実行でのオーバーヘッドが少ないこと
- プロファイラ自体が実行時間に大きな影響を与えてはならない
プロファイラによる情報の収集
Cレベルのスタックトレースを取得するためには、C 拡張のプロファイリングが必要なので、Pf2ではこれを実現するためにプログラムカウンタの記録しています。
加えて、以下のような命令を使用してスレッド情報の収集も行います。
- rb_profile_thread_frames
- 任意のスレッドを引数に取りスレッド情報を返す組み込み命令で、Pf2ではスタックトレースを取得するために使用される
- Tracepoint API
- Ruby 2.0 で導入された命令で、Pf2では GC のイベントをトレースするために使用される
- Thread Events API
- スレッドの状態が変化したときのフックで、Pf2では GVL の状態を監視するために使用される
これらの情報を収集することで、GC ステートを含めた様々なイベントをレポートに含めることができます。
プロファイルのスケジューリング
プロファイルのスケジューリング実装では、 timer_create(2)
システムコールを使用します。
これによってPf2 signal handler が SIGPROF シグナルを検知して、情報を収集します。
スケジューリングの間隔は、狭くするほど正確になる一方、オーバーヘッドが増加するので、適切な値を設定する必要があります。悩ましい状況の中、Pf2 のサンプリング間隔を19ms に決定した話は、実装者ならではの興味深いエピソードだと感じました。
また、スケジューリングのオーバーヘッドの比率は 5% (1s のうち 50 ms) 以内を目指しているとのことです。つまり各サンプリング毎に 0.95ms 以内で情報収集を行う必要があります、シビアですね。
サポートされている時間定義
- Real time 実時間
- CPU time 各コア単位で見た消費時間
- カーネルスレッドごとのCPU タイム
- 1:1 モデルを使用している場合は、Ruby スレッド1つに対しカーネルスレッドが1つ紐づきます。
2. 取得した情報の展開と集約
情報の展開
Pf2 はプログラム実行時のオーバーヘッドを抑えるため、ランタイム中は必要最小限の情報を収集します。そのため実行完了後に、収集した情報を展開してスタックを組み立てる後処理が必要になります。 必要な後処理を、オーバーヘッドを気にする必要がないタイミングに回すという工夫が面白いと思いました!
情報の集約
Ruby スタックと C スタックは別々に取得されるので、その2つを矛盾なく合成する後処理も必要です。 そのために Ruby スタックの中に CFUNC を発見したら、該当するCスタックを次に配置する、というようにフレームを合成していくことを繰り返して、最終的に1つのスタックを生成します。
3. 可視化
現状、Pf2 は既存の可視化ツールを使用しています。 スマートバンクさんのブログ記事を見るに、ビジュアライザに対する改善欲求も持っているようで、今後の展開が楽しみです。
Ruby プロファイラの未来
プロファイラはランタイムの実装に深く結びついています。 従って、Ruby 3.3 で導入された M:N スレッドモデルにも、今後対応していく必要があります。
また、 プロファイルソフトウェアは終わりなき戦いであるという話もされていました。 例えば、常にプロファイリングをONにした状態で実行する、という需要が増加しているとのことでした。
感想
Pf2 は Ruby プログラムのプロファイリングにおいて、強力なツールであると感じました。特に、C 拡張のプロファイリングに対応している点は、他のプロファイラにはない強みだと感じました。また、スピーカーの osyoyu さんの解説も非常にわかりやすく、初めて聞く人でも理解しやすい内容だったと思います。
あと、osyoyu さんが発表冒頭でおすすめしていた、スパムおにぎりを一度食べてみたいと思いました。
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。 jp.techouse.com