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

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

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

プローブ引数に関する留意事項

DTraceのsdtプローブの定義(※4)に関する説明には,プローブ呼び出しを行う際の引数に関する注意として,以下のように記述されています。

ポインタを間接参照せず,プローブ引数内の大域変数からロードしないようにすれば,無効時のプローブの影響を最小限に抑えることができます。ポインタの間接参照も,大域変数のロードも,Dのプローブ有効化アクション内で安全に実行できます。

少々言い回しが堅苦しいですが,プローブ呼び出しの引数に指定する値には,ポインタからの間接参照値や大域変数参照値を指定するなということです。

それでは,なぜ「ポインタからの間接参照値や大域変数参照値」を指定することが良くないのでしょうか?

間接参照や大域変数参照の際には,必ずメモリへのアクセスが発生します。そうすると,以下のような事象の発生する可能性が出てくるのです。

メジャーフォルト(major fault)の発生:
メジャーフォルトとは,アクセス先の仮想アドレスに対応するページが,物理メモリ上にない状態を指します。
ディスク領域(=スワップデバイス)に退避されている内容を物理メモリへと復旧させる作業が,カーネルによって実施されます
マイナーフォルト(minor fault)の発生:
マイナーフォルトとは,アクセス先の仮想アドレスに対応するページは物理メモリ上にはあるものの,対応する仮想アドレス~物理アドレス変換を行うための変換エントリが,MMU(Memory Management Unit)の管理領域に無い状態を指します。
仮想空間の管理テーブルからMMUへの変換エントリ充填作業がカーネルによって実施されます。

これらは共に,システムコール呼び出しのオーバヘッドなどとは比較にならないほどの処理コストを必要としますので,回避できるのであれば極力回避したい事象です。

先述した動作原理の都合から,独自プロバイダのプローブが無効化されていても,呼び出し引数の準備処理だけは実行されてしまいます。そのため,引数準備のための処理を契機として,上記の様なメジャー/マイナーフォルトが発生してしまう場合,プローブ呼び出しを追加したことで,プローブが無効化されていてもオーバヘッドが上乗せされてしまうわけです。

それでは,ポインタの間接参照値や大域変数参照値を使用したい場合はどうすればよいのでしょうか?

先に引用した文章では,⁠必要に応じてDスクリプトの中でアクセスすればよい」と述べられています。

Dスクリプトの中でアクセスしたからといって,メジャー/マイナーフォルトの発生が抑止されるわけではありません(※5)が,⁠情報採取しない場合のオーバヘッドは避けたいが,情報入手が必要な場合にコストを要するのは仕方がない」⁠ という考え方だと言えるでしょう。

ちなみに,カーネルのロードモジュール中の大域変数に関しては,参照する大域変数名の冒頭にバッククォート("`")を付けることで参照可能なのですが,ユーザプログラム中の大域変数に関しては,残念ながらこの方法でアクセスすることができません(※6)⁠

しかし,以下に説明するような一手間をかけてやることで,ユーザプログラム中の大域変数を参照できるようになります。

リスト10のDスクリプトは,対象コマンド名$1を置換)に加えて,参照対象となる変数のアドレスが指定$2を置換)されることを前提としています。変数シンボルからアドレス値を得るのではなく,アドレス値を直接指定するわけです。

リスト10 大域変数の参照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));
}

それでは,実際に使用する際の要領を図1に示します。

図1 大域変数の参照

$ nm 対象コマンド | grep globalvar
08060fe0 B globalvar
$ dtrace -s watch_globalvar.d \
        -c '対象コマンド ....' \
        対象コマンド \
        0x08060fe0
....

nmコマンド等を使用して,対象となる大域変数(この例では"globalvar")が配置されるアドレスを確認したなら,そのアドレスを使用してリスト10のDスクリプトを実行するわけです。

なお,文字列に関する大域変数を参照する場合は,変数の定義方式によって扱い方が異なります。

リスト11 文字列変数定義(1)

char string[] = "string;

大域変数stringがリスト11に類する形式で定義されている場合,この変数のアドレスは文字列データ格納領域の先頭アドレスそのものですから,stringのアドレスに対してcopyinstr()サブルーチンを適用することで,文字列データを読み込むことができます(リスト12)⁠

リスト12 文字列大域変数の参照(1)

pid$target:$1:main:entry
{
    printf("string=%s", copyinstr($2));
}

その一方で,大域変数stringがリスト13の形式で定義されている場合,この方法では文字列を参照することができません。

リスト13 文字列変数定義(2)

char* string = "string";

この場合の大域変数stringのアドレスは,文字列格納領域を指すポインタが格納されているアドレスですので,リスト14に示すDスクリプトのように,いったんアドレス情報をカーネル空間にコピーして,改めてそのアドレスからcopyinstr()サブルーチンを使用して,文字列データを読み込む必要があります。

リスト14 文字列大域変数の参照(2)

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スクリプトを使用する場合には,アプリケーション側で想定しているアドレスビット幅と,カーネル側でのそれを一致させる必要がありますので注意してください。詳細は,第7回での間接参照先の情報取得に関する説明を参照してください。

"char*"として参照に利用する分には,C/C++プログラム上ではどちらの変数も同じ様に扱うことができますが,両者の厳密な意味合いは全く異なるわけです。

※4)
ユーザプログラム向けの独自プロバイダは,sdt(Statically Defined Tracing)プロバイダと同じ原理で実現されていますので,独自プロバイダに関して詳細を知りたい方は,sdtプロバイダに関する説明もお読みください。
※5)
ユーザプログラムに対するDスクリプト適用の場合,ユーザ空間からカーネル空間へのデータコピーcopyinstr()copyinto()など)が必要になりますので,その分のオーバヘッド上乗せもあります
※6)
OpenSolaris 2009/06同梱版のDTraceでの状況です。将来は参照可能になるかもしれません。

おわりに

ユーザプログラムに対するDTraceの適用に関して,8回に渡って説明してきましたが,如何でしたでしょうか?

DTraceは本連載で紹介した機能以外にも,たとえばタイマーによる等間隔処理といった機能も提供していますので,ぜひSunが提供するドキュメントで詳細をご確認ください。

本連載が,DTrace利用の幅を広げるきっかけとしてお役に立てば幸いです。

著者プロフィール

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

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