プログラムのパフォーマンス解析

ロック lint の使い方

ロック lint を使用するには、以下の 3 つの手順が必要です。

  1. ロック lint を使用するための環境の設定

  2. 解析対象となるソースコードのコンパイルと、ロック lint データベースファイル (.ll ファイル)の作成

  3. lock_lint コマンドを使ったロック lint セッションの実行

この項の後半では、これらの手順について解説します。

図 5-1 に、ロック lint を使った一連のタスクの制御フローを示します。

図 5-1 ロック lint 制御フロー

Graphic

ロック lint を利用することで、システム実行のために整備すべきアサーションを改善できます。質の高いアサーションを整備することで、ロック lint は既存のソースコードおよび新しいソースコードの正当性を評価できるようになります。

ロック lint 環境の管理

ロック lint インタフェースは、シェルで実行される lock_lint コマンドと lock_lint サブコマンドから構成されます。デフォルトでは、ロック lint は環境変数 $SHELL によって指定されるシェルを使用します。あるいは、lock_lint start コマンドで使用するシェルを指定することによって、ロック lint は任意のシェルを実行できます。以下の例では、Korn シェルでロック lint セッションを開始しています。


% lock_lint start /bin/ksh

ロック lint は子シェル上で見られる LL_CONTEXT という環境変数を作成します。初期設定のために準備したシェルを使用する場合、ホームディレクトリの .ll_init ファイルを、lock_lint コマンドが読み込むようにアレンジし、カレントディレクトリの .ll_init ファイル (存在する場合は) を実行させることができます。csh を使用する場合は、.cshrc ファイルに以下のコードを挿入することで、こうした操作を実行できます。


if ($?LL_CONTEXT) then
     if (-x $(HOME)/.ll_init) source $(HOME)/.ll_init
endif

ロック lint を同じファイルに対して実行したいと考えるほかのユーザーが、同じシェルを使うとは限らないため、.cshrc に現在の作業ディレクトリのファイルを読み込ませないようにすることをお薦めします。ユーザーの $(HOME)/.ll_init を使用するのは 1 人だけであるため、ロック lint セッション中にプロンプトを変更し、使用するエイリアスを定義できるように、$(HOME)/.ll_init を読み込む形式とすべきです。以下の 〜/.ll_init バージョンは csh に対してこの操作を実行します。


# Cause analyze subcommand to save state before analysis.
alias analyze "lock_lint save before analyze;¥
    lock_lint analyze"
# Change prompt to show we are in lock_lint.
set prompt="lock_lint‾$prompt"

startも参照してください。

サブコマンドを実行する場合は、目的に応じて、パイプ、リダイレクト、逆引用符 (``) などを利用できます。たとえば、以下のコマンドは、ロック foo がすべてのグローバル変数を保護することをアサートしています (グローバル変数の公式名称はコロンで始まります)。


% lock_lint assert foo protects `lock_lint vars | grep ^:`

一般的に、サブコマンドは grepsed などのフィルタとともに簡単に利用できるように設定されます。特に、各変数や関数に対して 1 行分の情報を出力する varsfuncs には、こうした機能は欠かせません。各行には、変数や関数の (定義されたあるいは派生した) 属性が含まれます。以下の例は、構造体 bar のどのメンバーが、メンバー lock によって保護されるかを示します。


% lock_lint vars -a `lock_lint members bar` | grep =bar::lock

シェルインタフェースを使用するため、ユーザーコマンドのログは、シェルの履歴 (history) 機能を利用することで得られます (history のレベルは、.ll_init ファイルにおいて、大きめに設定する必要があるかもしれません)。

一時ファイル

$TMPDIR が設定されていない限り、ロック lint は /var/tmp に一時利用ファイルを出力します。

メークファイルの規則

.ll ファイルを生成するように メークファイルを修正するには、まずは .c から .o を作成するための規則を使って、.c から .ll を作成するルールを記述します。たとえば、以下の例から、その次の例を記述できます。


# Rule for making .o from .c in ../src.
%.o: ../src/%.c
    $(COMPILE.c) -o $@ $<


# Rule for making .ll from .c in ../src.
%.ll: ../src/%.c
    cc $(CFLAGS) $(CPPFLAGS) $(FOO) $<

上記の例では、コンパイラオプション (CFLAGSCPPFLAGS) 用のメークマクロに -Zll フラグが指定されていなければなりません。

接尾辞規則を使用する場合、.ll をサフィックスとして定義する必要があります。そのため、ユーザーによっては、% ルールの使用を好むかもしれません。

適切な .o ファイルが make 変数 FOO_OBJS に含まれている場合、以下の行によって FOO_LLS を作成できます。


FOO_LLS = ${FOO_OBJS:%.o=%.ll}

あるいは、それらがサブディレクトリ ll にある場合は、以下のようになります。


FOO_LLS = ${FOO_OBJS:%.o=ll/%.ll}

サブディレクトリ ll/.ll ファイルを保存したい場合、以下のラベルにより、メークファイルに自動的にこのファイルを作成させることができます。


.INIT:
     @if [ ! -d ll ]; then mkdir ll; fi

コードのコンパイル

ロック lint でソースコードを解析するには、まずは Sun WorkShop ANSI C コンパイラの -Zll オプションを使ってこれをコンパイルする必要があります。次に、コンパイラはコンパイルされた各 .c ファイルに 1 つずつロック lint データベースファイル (.ll ファイル) を作成します。そして、この .ll ファイルは、後ほど load サブコマンドによってロック lint に読み込みます。

ときには、ロック lint は、解析中に意味のある結果を返すために、単純化したコードを必要とする場合があります。こうしたシンプルな表示を提供できるようにするために、-Zll オプションは自動的にマクロ __lock_lint を定義します。

__lock_lint の使用法については、 「ロック lint の制限事項」 の項で詳しく解説します。

ロック lint サブコマンド

ロック lint インタフェースは、lock_lint コマンドおよびそのコマンドと一緒に指定することが可能な一連のサブコマンドから構成されます。


lock_lint [subcommand]

この例の subcommand の部分には、ソースコードを解析し、データ競合およびデッドロックを調べるために使用される一連のサブコマンドを指定します。サブコマンドの詳細については、付録 A 「ロック lint コマンドリファレンス」を参照してください。

ロック lint の起動と終了

ロック lint セッションにおける最初のサブコマンドは、いつも start でなければなりません。このサブコマンドが、ロック lint の利用状況に応じてユーザーが選択したサブシェルを起動します。ロック lint セッションはサブシェル内から開始されるため、このサブシェルを終了することでセッションは終了します。たとえば、C シェルを使っている場合、ロック lint を終了するには、コマンド exit を使用します。

ツール状態の設定

ロック lint の状態は、読み込まれる一連のデータベースと指定されたアサーションから構成されます。そして、この状態の修正と解析の再実行を繰り返すことで、潜在的なデータ競合およびデッドロックを最適化するための情報を入手できます。1 つの状態に対して、解析はつねに 1 度だけ可能です。状態を再確立し、その状態に修正を加え、再度解析を試みるための手段としては、saverestore および refresh の各サブコマンドが提供されています。

アプリケーションをチェックする
  1. ソースコードに注釈を挿入し、それをコンパイルして .ll ファイルを作成します。

    「ソースコードへの注釈の挿入」の項を参照してください。

  2. load サブコマンドを使って .ll ファイルを読み込みます。

  3. assert サブコマンドを使って、関数および変数を保護するロックについてのアサーションを設定します。


    注 -

    これらの指定は、ソースコードへの注釈の挿入を使ってロック lint に伝えることもできます。「ソースコードへの注釈の挿入」の項を参照してください。


  4. assert order サブコマンドを使って、デッドロックを回避するためには、ロックをどのような順番で獲得するかについてのアサーションを指定します。


    注 -

    これらの指定は、ソースコードへの注釈の挿入を使ってロック lint に伝えることもできます。「ソースコードへの注釈の挿入」を参照してください。


  5. どの関数がルートであるかについてロック lint が正しく認識しているかをチェックします。

    funcs -o サブコマンドがルート関数をルート (基点) として提示しない場合は、declare root サブコマンドを使って修正してください。funcs -o が非ルート関数をルートとして提示する場合は、declare... targets サブコマンドを使ってその関数を関数ターゲットとしてリストアップしている可能性が考えられます。ルート関数の詳細については、declare root funcを参照してください。

  6. assert rwlock サブコマンドを使って、階層的ロック関係 (存在する場合。ただし、まれです) を記述します。


    注 -

    これらの指定は、ソースコードへ注釈の挿入を使ってロック lint に伝えることもできます。「ソースコードへの注釈の挿入」 を参照してください。


  7. ignore サブコマンドを使って、解析から除外したい関数および変数を無視します。

    ignore コマンドの使用はできるだけ控えてください。このサブコマンドに相当するソースコードへの注釈の挿入 (例:NO_COMPETING_THREADS_NOW) もできる限り使用しないでください。

  8. analyze サブコマンドを使って解析を実行します。

  9. エラーを処理します。

    この操作には、#ifdef__lock_lint (「ロック lint の制限事項」を参照) を使ったソースの修正、あるいは手順 3、4、6、7 を遂行するためのソースコードへの注釈の挿入 (「ソースコードへの注釈の挿入」を参照) などが含まれます。

    ロック lint を解析以前の状態に戻し、必要ならば解析を再実行してください。


    注 -

    エラーは順番に処理することがベストです。順番が逆になると、関数のエントリに際して保持されないロックの問題、あるいは保持されていないのに解放されてしまうロックの問題によって、正常に保護されない変数についての誤ったメッセージが多数生じてしまいます。


  10. analyze -v サブコマンドを使った解析を実行して、上記の手順を繰り返します。

  11. analyze サブコマンドからのエラーがなくなったら、いかなるロックからも正常に保護されない変数をチェックします。

    以下のコマンドを使用してください。

lock_lint vars -h | fgrep ¥*

適切なアサーションを使った解析を再実行し、どこで、適切にロックされないまま変数がアクセスされているかを見つけてください。

まったく同じ状態に対して analyze を 2 回実行することはできないので、analyze を実行する前に、save サブコマンドを使ってロック lint の状態を保存しておくことをお薦めします。また、ほかのアサーションを追加する前に、refresh または restore を使ってその状態を復元してください。analyze 前に自動的に保存を実行する、analyze 用のエイリアスを設定しておくと良いでしょう。

プログラム情報の管理

ロック lint は、C コンパイラによって作成される一連のデータベースによって、解析対象となるソースについての情報を獲得します。各ソースファイルのロック lint データベースは、独立したファイルに保存されます。一連のソースファイルを分析するには、load サブコマンドを使用してそれらに関連するデータベースファイルを読み込みます。files サブコマンドは、読み込まれたデータベースファイルによって表されるソースファイルのリストの表示に使用できます。いったんファイルが読み込まれると、ロック lint は、すべての関数、グローバルデータ、関連ソースファイルにおいて参照される外部関数についての情報を得るようになります。

関数管理

解析フェーズの一部として、ロック lint は、読み込まれたすべてのソースに対してコールグラフを作成します。定義された関数についての情報は、funcs サブコマンドを介して利用可能となります。ロック lint が解析対象であるコードに対する正確なコールグラフを持つということは、意味のある解析を行う上でとても重要です。

読み込まれたファイルから呼び出されることのない関数はすべてルート関数と呼ばれます。読み込まれたモジュール内で呼び出される関数であっても (たとえば、関数がライブラリのエントリポイントであり、ライブラリ内でも呼び出される場合など)、ルート関数として扱いたいこともあります。そのためには、declare root サブコマンドを使用してください。また、ignore サブコマンドを実行することによって、コールグラフから関数を削除することも可能です。

ロック lint は、関数ポインタに対するすべての参照およびそれらに対して行なわれる関数の割り当て状況について知っています。現在読み込まれているファイルの関数ポインタについての情報は、funcptrs サブコマンドを介して利用できます。関数ポインタを経由して行われる呼び出しについての情報は、pointer calls サブコマンドを介して利用できます。もしも、ロック lint が発見できないような関数ポインタの割り当てが存在する場合、それらは declare ... targets サブコマンドによって指定されている可能性があります。

デフォルトでは、ロック lint は可能性のあるすべての実行パスの検証を行おうとします。そして、コードが関数ポインタを使用している場合、コードの通常の動作においては、実行パスの多くが実際にはたどられない可能性があります。これにより、実際には起こらないデッドロックが報告されてしまうことがあります。こうした事態を防ぐには、disallow および reallow サブコマンドを使用して、決して使用されることのない実行パスをロック lint に知らせてください。また、reallows および disallows サブコマンドを使用することによって、現時点での制限事項を出力できます。

変数管理

ロック lint データベースにも、ソースコード内でアクセスされるすべてのグローバル変数についての情報が含まれます。これらの変数についての情報は、vars サブコマンドを介して利用できます。

ロック lint のジョブの 1 つは、変数のアクセスが整合性を保って保護されているかどうかを確認することです。特定の変数についてそのアクセスが問題にならない場合は、ignore サブコマンドによって、検証の対象から除外できます。

また、次に示すソースコードへの注釈の挿入のどれか1つを利用することも可能です。

SCHEME_PROTECTS_DATA

READ_ONLY_DATA

DATA_READABLE_WITHOUT_LOCK

NOW_INVISIBLE_TO_OTHER_THREADS

NOW_VISIBLE_TO_OTHER_THREADS

詳細は、 「ソースコードへの注釈の挿入」を参照してください。

ロック管理

ソースコードへの注釈の挿入を利用することで、コード中のロックに関するアサーションを効率的に調整できます。アサーションには、保護、順序、副作用の 3 種類があります。

保護アサーションは、特定のロックによって何が保護されているかを説明します。以下に示すソースコードへの挿入を使用してください。

MUTEX_PROTECTS_DATA

RWLOCK_PROTECTS_DATA

SCHEME_PROTECTS_DATA

DATA_READABLE_WITHOUT_LOCK

RWLOCK_COVERS_LOCK

assert サブコマンドのバリエーションは、特定のロックがデータまたは関数の一部分を保護していることをアサートするために使用されます。さらに別のバリエーションである assert ... covers は、特定のロックがほかのロックを保護していることをアサートします (これは階層的ロッキングスキームに使用されます)。

順序アサーションは、特定のロックが取得されるべき順序を指定します。ソースコードへの注釈の挿入 LOCK_ORDER または assert order サブコマンドによって、ロックの順序を指定できます。

副作用アサーションは、ある関数が特定のロックの解放または取得の副作用を持つことをアサートします。 以下に示すソースコードへの注釈の挿入を使用してください。

MUTEX_ACQUIRED_AS_SIDE_EFFECT

READ_LOCK_ACQUIRED_AS_SIDE_EFFECT

WRITE_LOCK_ACQUIRED_AS_SIDE_EFFECT

LOCK_RELEASED_AS_SIDE_EFFECT

LOCK_UPGRADED_AS_SIDE_EFFECT

LOCK_DOWNGRADED_AS_SIDE_EFFECT

NO_COMPETING_THREADS_AS_SIDE_EFFECT

COMPETING_THREADS_AS_SIDE_EFFECT

assert side effect サブコマンドによって副作用を指定することも可能です。ときには、外部関数についての副作用アサーションを作成したいこともあり、その場合、ロックは読み込まれたモジュールから可視ではありません (たとえば、外部関数のモジュールに対して静的である場合など)。このようなケースでは、declare サブコマンドの形式を使用することで、ロックを“作成”できます。

ロックの使用状況の解析

ロック lint の主たる役割は、データ競合やデッドロックに通じる可能性のある整合性に欠けたロックの使用状況を報告することです。ロックの使用状況の解析は、analyze サブコマンドの使用時に行われます。その結果として、以下の問題が報告されます。

解析後のクエリー

解析の後、ロック lint のサブコマンドによって以下の操作が可能となります。

そうしたサブコマンドの 1 つが order であり、ロックが獲得された順序に関する調査に使用できます。ロック lint が潜在的なデッドロックをより正確に診断できるように、ロックの順序に関する問題を理解し、そうした順序についてのアサーションを作成するためには、こうした情報は有効です。

さらに、同様のサブコマンドには vars があります。vars サブコマンドは、変数の読み取りまたは書き込み時 (存在する場合)、どのロックが整合性を保持するかについて報告します。保護に関する規約がもともとドキュメント化されていなかったり、古くなってしまっているコードにおいて、保護規約を確定するためには、こうした情報は有効です。

ロック lint の制限事項

ロック lint の解析力にも限界はあります。こうした限界の最大の要因は、ロック lint がユーザーの変数の値を知らないという現実にあります。

ロック lint は、ありがちな原因を無視したり、想定を単純化することで、さまざまな種類の問題を解決します。その他の問題も、アプリケーションの中で条件付きでコンパイルされたコードを使用することで回避できます。-Zll オプションを使ってコンパイルを行うと、コンパイラはつねにプリプロセッサマクロ __lock_lint を定義します。このマクロを利用することで、コードがあいまいになることをかなり回避できます。

ロック lint は以下の問題についての推論ができません。

その他のロック lint の問題点には以下のものがあります。

解析中、ロック lint は rw_upgrade と呼ばれるロック操作についてのメッセージを生成する場合もあります。そうした呼び出しは現実には存在しませんが、ロック lint は以下のようなコードをその次のコードのように書き直します。


if (rw_tryupgrade(&lock1)) {  ...  }


if () { rw_tryupgrade(&lock1);	... }

rw_tryupgrade() が起こる場所では、ロック lint は常にそれが成功しているものとみなします。

すでに保持されているロックを獲得しようとした場合、ロック lint はエラーとしてフラグを立てます。しかし、ロックに名前未定の場合 (たとえば、foo::lock)、この名前は単独のロックではなく、一連のロックを参照するため、こうしたエラーは抑制されます。しかし、名前未定のロックが常に同じロックを参照する場合は、ロック lint がこの種の潜在的デッドロックを報告できるように、declare one サブコマンドを使ってください。

これらのロックから独自のロックを構築した場合 (たとえば、再帰的な相互排他はときに通常の相互排他から構築されます)、ロック lint はそれらについて知ることはありません。一般的には、#ifdef を使って、通常の相互排他が操作されているかのようにロック lint に提示します。名前未定のロックが再帰的にロックされても、エラーが生成されることはないので、再帰的なロックに対しては、名前未定のロックを使用してください。以下に例を示します。


void get_lock() {
    #ifdef __lock_lint.  
        struct bogus *p;
        pthread_mutex_lock(p->lock);
    #else
        <the real recursive locking code>
    #endif
}