DTrace は
ただし、
そこで今回は、
アセンブラレベルから見た独自プロバイダのオーバヘッド
独自プロバイダの実現方式
独自プロバイダの実現方式について説明するに当たって、
独自プロバイダのプローブ呼び出しを行うソースをコンパイルした直後、
subl $0x8,%esp ; スタックフレーム拡張
pushl $0xa ; 第2引数(4バイト)の準備
pushl $0x0 ; 第1引数(4バイト)の準備
call __dtrace_checkpoint___pass
addl $0x10,%esp ; 引数領域の解放
このオブジェクトファイルに対して、dtrace -G
" 実行によるリンク前処理を行うことで、
subl $0x8,%esp ; スタックフレーム拡張
pushl $0xa ; 第2引数(4バイト)の準備
pushl $0x0 ; 第1引数(4バイト)の準備
nop
nop
nop
nop
nop
addl $0x10,%esp ; 引数領域の解放
"call __
"に必要な5バイトの領域が、nop
によって置き換えられているのがわかります。
この前処理によって、nop
によって確保されたままです。つまり、
第3回での pid
プロバイダの実現方式確認の手順を利用すれば、
プローブ無効化方式に関する考察
先述したように、dtrace -G
"実行によるリンク前処理によって、__
"関数の呼び出し」
つまり、
そのため、
プローブ活性に応じて条件分岐させることで、プローブ呼び出し処理(引数準備含む)を抑止した方が、性能劣化が少ないのでは?
C 言語的に書くならば、
/* プローブ有効時には probe_enabled を 1 に書き換え */
if(probe_enabled){
PROVIDER_PROBE(arg0, arg1, arg2, ....);
}
....
しかし、
そうなると、
プローブ活性に応じて無条件分岐命令を埋め込むことで、パイプラインを乱れさせることなくプローブ呼び出し処理を抑止できるのでは?
C言語的に書くならば、
/* プローブ有効時には goto 文を nop に書き換え */
goto probe_disabled;
PROVIDER_PROBE(arg0, arg1, arg2, ....);
probe_disabled:
....
実行効率の点から見た場合、
今時のコンパイラの最適化処理に掛かると、
また、
以上のことから、
プローブ埋め込みによるオーバヘッドの計測
では実際のところ、
単純に実行時間で比較したのでは、
- 独自プローブに渡す引数は6つ
- プローブ非活性状態で、
引数準備処理だけを1012回繰り返す - アセンブラレベルで引数準備処理を除外したものを同じように1012回繰り返す
- 実行時間の差分を取る
- ループ1回あたりの性能劣化をクロック数に変換
以下の2つのCPUで計測しました。
- UltraSPARC IIIi
(1. 0GHz) - AMD PhenomII X4 905e
(2. 5GHz)
計測結果を以下に示します。
環境 | 性能低下 | 引数渡しの方式 |
---|---|---|
SPARC | 2. | レジスタ |
x86 | 6. | スタック |
x86 | 0. | レジスタ |
SPARC アーキテクチャや 64bit x86(AMD64)における性能劣化が小さいのは、
6つの引数を積むオーバヘッドがそれぞれ2.
その一方で、
(CPU クロックと比較して)
この結果を見る限りでは、
なおここでの計測結果は、
プロバイダ有効時の留意事項
pid
プロバイダによる性能劣化
これまでにも何度か触れてきましたが、
記述された処理の実行が必要になる都度、
そのため、pid
プロバイダを使用して、
Dスクリプトにおいて処理の実施を制限する方法としては、
たとえばリスト5のように記述した場合、command
中の全ての関数の呼び出しを契機にしてカーネルへのコンテキストスイッチが発生します。
pid$target:command::entry
{ .... }
その一方でリスト6のように記述した場合、funcA
の呼び出しだけに限定されるため、
pid$target:command:funcA:entry
{ .... }
Dスクリプトにおいて処理の実施を制限するもうひとつの方法として、
たとえば、printf()
"アクションの実施は、self->traced
"が成立している場合、doit()
"の開始から終了の間に限定されます。
pid$target:$1:doit:entry
{ self->traced = 1; }
pid$target:$1::entry,
pid$target:$1::return
/self->traced/
{ printf("%s():%s", probefunc, probename); }
pid$target:$1:doit:return
{ self->traced = 0; }
"printf()
"アクションの実施契機が減少することから、
- 全ての関数の冒頭でいったんカーネルにコンテキストスイッチ
- 条件 "
self->traced
" が成立しているかを判定 - 条件が成立している場合はアクションを実行
という手順が踏まれるため、
もしも性能劣化を極力回避したい状況で、
独自プロバイダ有効時の性能劣化抑止
独自プロバイダ定義からヘッダファイルを生成dtrace -h
"実行)checkpoint
プロバイダのpass
プローブの例)。
#define CHECKPOINT_PASS_ENABLED() \
__dtraceenabled_checkpoint___pass()
マクロ名はENABLED
」
このマクロはプローブ呼び出しに使用する引数の準備が非常にコスト高な場合に、
if(XXXX_YYYY_ENABLED()){
XXXX_YYYY(calc_complex_value());
}
たとえば、XXXX_
プローブ呼び出しにおいて、calc_
関数が非常にコスト高だとしましょう。
先に述べたように、
そのため、XXXX_
"判定を使用しない場合、calc_
関数が実行されてしまうことから、
その一方で、
なお、
プローブ引数に関する留意事項
DTraceのsdt
プローブの定義
ポインタを間接参照せず、プローブ引数内の大域変数からロードしないようにすれば、無効時のプローブの影響を最小限に抑えることができます。ポインタの間接参照も、大域変数のロードも、Dのプローブ有効化アクション内で安全に実行できます。
少々言い回しが堅苦しいですが、
それでは、
間接参照や大域変数参照の際には、
- メジャーフォルト
(major fault) の発生: - メジャーフォルトとは、
アクセス先の仮想アドレスに対応するページが、 物理メモリ上にない状態を指します。
ディスク領域(=スワップデバイス) に退避されている内容を物理メモリへと復旧させる作業が、 カーネルによって実施されます - マイナーフォルト(minor fault)の発生:
- マイナーフォルトとは、
アクセス先の仮想アドレスに対応するページは物理メモリ上にはあるものの、 対応する仮想アドレス~物理アドレス変換を行うための変換エントリが、 MMU (Memory Management Unit) の管理領域に無い状態を指します。
仮想空間の管理テーブルからMMUへの変換エントリ充填作業がカーネルによって実施されます。
これらは共に、
先述した動作原理の都合から、
それでは、
先に引用した文章では、
Dスクリプトの中でアクセスしたからといって、
ちなみに、`
")
しかし、
リスト10のDスクリプトは、$1
を置換)$2
を置換)
watch_globalvar.d
)self int* buf;
pid$target:$1::entry
{
self->buf = alloca(sizeof(int));
copyinto($2, sizeof(int), self->buf);
printf("%d", *(self->buf));
}
それでは、
$ nm 対象コマンド | grep globalvar 08060fe0 B globalvar $ dtrace -s watch_globalvar.d \ -c '対象コマンド ....' \ 対象コマンド \ 0x08060fe0 ....
nm
コマンド等を使用して、globalvar
")
なお、
char string[] = "string;
大域変数string
がリスト11に類する形式で定義されている場合、string
のアドレスに対してcopyinstr()
サブルーチンを適用することで、
pid$target:$1:main:entry
{
printf("string=%s", copyinstr($2));
}
その一方で、string
がリスト13の形式で定義されている場合、
char* string = "string";
この場合の大域変数string
のアドレスは、copyinstr()
サブルーチンを使用して、
self uintptr_t* buf;
pid$target:$1:main:entry
{
self->buf = alloca(sizeof(uintptr_t));
copyinto($2, sizeof(uintptr_t), self->buf);
printf("string=%s", copyinstr(*(self->buf)));
}
リスト14のDスクリプトを使用する場合には、
"char*
"として参照に利用する分には、
おわりに
ユーザプログラムに対するDTraceの適用に関して、
DTraceは本連載で紹介した機能以外にも、
本連載が、