DTrace は「オーバヘッドが低い」,場合によっては「オーバヘッドが殆ど無い」といった表現をされることがありますが,付加的な処理=情報採取を行うにあたり,オーバヘッドが無いということは有り得ません。
ただし,DTraceの動作原理等を踏まえた上で適切な使い方をすることで,余分なオーバヘッドを抑止することはできます。
そこで今回は,DTraceを利用する際の性能劣化に着目して,どのような点に留意すべきかを説明したいと思います。
アセンブラレベルから見た独自プロバイダのオーバヘッド
独自プロバイダの実現方式
独自プロバイダの実現方式について説明するに当たって,第6回で説明したリンク時手順を振り返ってみましょう。
独自プロバイダのプローブ呼び出しを行うソースをコンパイルした直後,つまりオブジェクトファイルを生成した時点では,プローブ呼び出し部分は通常の関数呼び出しと同様に,引数準備を行ったうえでプローブに相当する関数の呼び出しを行っています。32ビットx86環境で生成されるオブジェクトファイルの逆アセンブル結果をリスト1に示します(※1)。
リスト 1 プローブ呼び出しのアセンブラコード
subl $0x8,%esp ; スタックフレーム拡張
pushl $0xa ; 第2引数(4バイト)の準備
pushl $0x0 ; 第1引数(4バイト)の準備
call __dtrace_checkpoint___pass
addl $0x10,%esp ; 引数領域の解放
このオブジェクトファイルに対して,"dtrace -G" 実行によるリンク前処理を行うことで,リスト 2 のようにプローブ呼び出し処理が除去されます。
リスト 2 前処理後のアセンブラコード
subl $0x8,%esp ; スタックフレーム拡張
pushl $0xa ; 第2引数(4バイト)の準備
pushl $0x0 ; 第1引数(4バイト)の準備
nop
nop
nop
nop
nop
addl $0x10,%esp ; 引数領域の解放
"call __dtrace_checkpoint___pass"に必要な5バイトの領域が,全てnopによって置き換えられているのがわかります。
この前処理によって,プローブ呼び出しそのものは実施されなくなりますが,プローブ呼び出しに必要な命令を埋め込む領域はnop によって確保されたままです。つまり,プローブ呼び出しを有効化したくなった際には,この領域にプローブ呼び出しを行う命令を埋め込むことで,プローブの活性をプログラム実行中に動的に変更できるわけです。
第3回での pidプロバイダの実現方式確認の手順を利用すれば,DTrace 適用時の上記挙動を確認することができます。
- ※1)
disコマンド等による実際の逆アセンブル出力では,シンボル解決が未実施であるため,"call __dtrace_checkpoint___pass"ではなく,"call -0x4 <main+0x2d>"といった形式で出力されます。
また,ここでの例における第1引数は文字列領域へのアドレスですが,これもシンボル解決が未実施であるため,具体的なアドレス値ではなく0x0が設定されています。
プローブ無効化方式に関する考察
先述したように,"dtrace -G"実行によるリンク前処理によって,「プローブ呼び出し」に相当する「"__dtrace_*"関数の呼び出し」は除去されますが,前処理前後の逆アセンブル結果を比較すればわかるように,プローブ呼び出しに伴う引数準備処理は残されたままです。
つまり,独自プロバイダのプローブ呼び出しが埋め込まれているプログラムは,プローブ呼び出しをしない場合と比較すれば,当然いくらかのオーバヘッドが上積みされているわけです。
そのため,以下のように考える方がいるかもしれません。
プローブ活性に応じて条件分岐させることで,プローブ呼び出し処理(引数準備含む)を抑止した方が,性能劣化が少ないのでは?
C 言語的に書くならば,リスト3のようなイメージでしょうか?
リスト3 プローブ無効化案(1)
/* プローブ有効時には probe_enabled を 1 に書き換え */
if(probe_enabled){
PROVIDER_PROBE(arg0, arg1, arg2, ....);
}
....
しかし,このような条件分岐を行った場合,命令実行パイプラインの乱れが発生してしまうため,条件分岐によって節約される命令実行コストと,条件分岐命令そのものによって増加するコストには,期待される程には差が生じません。
そうなると,今度は以下のように考える方も居ることでしょう。
プローブ活性に応じて無条件分岐命令を埋め込むことで,パイプラインを乱れさせることなくプローブ呼び出し処理を抑止できるのでは?
C言語的に書くならば,リスト4のようなイメージでしょうか?
リスト4 プローブ無効化案(2)
/* プローブ有効時には goto 文を nop に書き換え */
goto probe_disabled;
PROVIDER_PROBE(arg0, arg1, arg2, ....);
probe_disabled:
....
実行効率の点から見た場合,この方法はなかなか良さそうに思えますが,実は大きな問題があります。それはコンパイラの最適化処理等を考慮する必要があることです。
今時のコンパイラの最適化処理に掛かると,必ずしもプログラム中の各ステートメントと綺麗に対応関係の取れたアセンブラコードが生成されるわけではありません。あるステートメントを無効化するつもりで無条件分岐命令を埋め込んだ場合に,前後のステートメントに関連する部分まで無効化してしまう可能性もゼロではありません。
また,事前に(=コンパイル段階で)無条件分岐命令が埋め込まれるようにすると,最適化レベルによっては,プローブ呼び出し部分がコンパイルの際に除外されてしまう可能性もあります。
以上のことから,コンパイラとは独立した実現を前提とした場合,現状の独自プロバイダの実現方式は,最適とは言えないものの妥当な実現方式ではあると言えるでしょう。
プローブ埋め込みによるオーバヘッドの計測
では実際のところ,DTraceのプローブ埋め込みにより,どの程度のオーバヘッドが生じるのでしょうか?
単純に実行時間で比較したのでは,計測環境におけるCPUクロックの高低による影響があることから,以下の手順で遅延クロック数を算出することにしました。
- 独自プローブに渡す引数は6つ
- プローブ非活性状態で,引数準備処理だけを1012回繰り返す
- アセンブラレベルで引数準備処理を除外したものを同じように1012回繰り返す
- 実行時間の差分を取る
- ループ1回あたりの性能劣化をクロック数に変換
以下の2つのCPUで計測しました。
- UltraSPARC IIIi(1.0GHz)
- AMD PhenomII X4 905e(2.5GHz)
計測結果を以下に示します。
表1 プローブ埋め込みによる遅延の計測
| 環境 | 性能低下 (単位: clocks/loop) | 引数渡しの方式 |
|---|---|---|
| SPARC | 2.0 | レジスタ |
| x86(32bit) | 6.0 | スタック |
| x86(64bit) | 0.1 | レジスタ |
SPARC アーキテクチャや 64bit x86(AMD64)における性能劣化が小さいのは,引数渡しにレジスタを使用する ABI(Application Binary Interface)である(※2)ことから,使用頻度の高い(=レジスタに乗り易い)局所変数値の引数渡しであれば,レジスタ間コピーのみのオーバヘッドで済むためだと思われます。
6つの引数を積むオーバヘッドがそれぞれ2.0と0.1(!)ですから,スーパースケーラ(superscalar)アーキテクチャの面目躍如といったところでしょうか?
その一方で,32bit x86アーキテクチャの性能劣化が大きいのは,引数渡しにスタックを使用する仕様であるため,局所変数参照でもスタック(=メモリ)からの書き込みが発生するためでしょう。
(CPU クロックと比較して)低速なメモリへのアクセスがあるにも関わらず,概ね引数の数と同程度のクロックの遅延で済んでいるのは,おそらくスタック領域がデータキャッシュに乗っていることによるものだと思われます。
この結果を見る限りでは,64ビットx86においてなら(SPARCも?)「DTrace無効時は,プローブ埋め込みによるオーバヘッドはほぼゼロ」と言っても差し支え無いかも?という印象ですね。
なおここでの計測結果は,あくまで(単純化した)特定のモデルにおける性能劣化の計測値であって,常に同等の計測結果が得られるわけではありませんからご注意ください。
- ※2)
- AMD64形式アプリケーションのABIがレジスタ渡しを採用していることは,DTrace Day 2010.03に参加された方から教えていただきました。ありがとうございます。

