C/C++プログラマのためのDTrace入門

第8回 DTrace併用時の性能劣化

この記事を読むのに必要な時間:およそ 6 分

プロバイダ有効時の留意事項

pidプロバイダによる性能劣化

これまでにも何度か触れてきましたが,DTraceは指定されたDスクリプトをカーネル空間で実行しています。

記述された処理の実行が必要になる都度,カーネルへのコンテキストスイッチが発生してしまうことから,通常の関数呼び出しと比較して格段に大きなオーバヘッドを伴います。

そのため,たとえばpidプロバイダを使用して,全ての関数の呼び出し・復帰を採取するようなDスクリプトを適用した場合,各関数の冒頭/末尾で毎回システムコールを発行するようなものですから,DTraceを併用しない通常の実行に比べれば,対象プログラムの実行性能は大きく低下することでしょう(※3)。

Dスクリプトにおいて処理の実施を制限する方法としては,まず第一に「対象関数を明示して絞り込む」方法があります。

たとえばリスト5のように記述した場合,command 中の全ての関数の呼び出しを契機にしてカーネルへのコンテキストスイッチが発生します。

リスト5 全関数がコンテキストスイッチ契機

pid$target:command::entry
{ .... }

その一方でリスト6のように記述した場合,カーネルへのコンテキストスイッチが発生しするのは関数funcAの呼び出しだけに限定されるため,リスト5の記述と比較してオーバヘッドを大幅に低減させることができます。

リスト6 指定関数のみがコンテキストスイッチ契機

pid$target:command:funcA:entry
{ .... }

Dスクリプトにおいて処理の実施を制限するもうひとつの方法として,⁠述語(前提条件)を記述する」方法があります。

たとえば,リスト7のように記述することで,"printf()"アクションの実施は,"self->traced"が成立している場合,つまり関数"doit()"の開始から終了の間に限定されます。

リスト7 述語記述による絞込み

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()"アクションの実施契機が減少することから,この方法も一見すると効率が良いように見えますが,実際のDスクリプトの実行では:

  1. 全ての関数の冒頭でいったんカーネルにコンテキストスイッチ
  2. 条件 "self->traced" が成立しているかを判定
  3. 条件が成立している場合はアクションを実行

という手順が踏まれるため,述語記述による「アクション実行」の絞込みは「カーネルへのコンテキストスイッチ」という大きなオーバヘッドの削減には効果がありません。

もしも性能劣化を極力回避したい状況で,アクションの実行対象となる関数が絞り込めるのであれば,明示的に対象関数を列挙することをお勧めします。

※3)
ただし,性能劣化の度合いは対象アプリケーションの特性にも依存します。
多数の関数を呼び出して純粋にデータ処理のみを行うアプリケーションは,おそらく性能劣化が著しいでしょうが,その一方で対話処理や外部I/Oの比率が高いアプリケーションの場合は,それほど性能劣化が生じない可能性があります。

独自プロバイダ有効時の性能劣化抑止

独自プロバイダ定義からヘッダファイルを生成("dtrace -h"実行)すると,リスト8に示すようなマクロ定義が含まれているはずですcheckpointプロバイダのpassプローブの例⁠⁠。

リスト8 活性判定マクロ定義

#define CHECKPOINT_PASS_ENABLED() \
        __dtraceenabled_checkpoint___pass()

マクロ名は「プロバイダ名」「プローブ名」ENABLEDという形式で構成されます。

このマクロはプローブ呼び出しに使用する引数の準備が非常にコスト高な場合に,プローブ活性の有無に応じてプローブ呼び出し=引数の準備を抑止するのに使用します。

リスト9 活性判定マクロの使用例

if(XXXX_YYYY_ENABLED()){
    XXXX_YYYY(calc_complex_value());
}

たとえば,リスト9 のXXXX_YYYYプローブ呼び出しにおいて,第1引数を算出するcalc_complex_value()関数が非常にコスト高だとしましょう。

先に述べたように,プローブが無効化されていても引数準備処理は実施されます。

そのため,"XXXX_YYYY_ENABLED()"判定を使用しない場合,プローブ呼び出しの要否に関わらず,コスト高なcalc_complex_value()関数が実行されてしまうことから,実行性能は全般的に低下してしまいます。

その一方で,プローブ埋め込みの際にリスト9のような実装を行った場合,プローブが無効化されている=情報採取の必要性が無ければ,プローブ呼び出しに関わる全ての処理が回避されますので,情報採取をしない場合の実行時性能劣化を最小限に抑えることができます。

なお,プローブの有効化が関数単位でできるのと同様に,プローブの活性も各関数ごとに個別に判定することができます。

著者プロフィール

藤原克則(ふじわらかつのり)

Mercurial三昧の日々が嵩じて, いつの間にやら『入門Mercurial Linux/Windows対応』を上梓。凝り性なのが災いして,年がら年中アップアップな一介の実装屋。最近は仕事の縁が元で,OpenSolarisに入れ込む毎日。