速度

NGen の強力な新機能によるアプリケーションのパフォーマンス向上

Reid Wilkes


翻訳元: NGen Revs Up Your Performance with Powerful New Features (英語)

この記事は、.NET Framework 2.0 のプレリリース バージョンに基づいています。この記事に含まれるすべての情報は、変更される可能性があります。

この記事で使用する技術: .NET Framework と CLR

この記事で取り上げる話題:

  • NGen によるマネージ アプリケーションのパフォーマンス向上
  • NGen が適しているかどうかを知る方法
  • .NET Framework 2.0 における NGen の新機能

目次

  1. NGen 2.0 の登場
  2. NGen が適している場合
  3. NGen のハードバインディング
  4. 新しい NGen の使用方法
  5. すべてを組み合わせた例
  6. シャットダウン
  7. 補足記事: NGen イメージのベース アドレス
  8. 補足記事: ネイティブ イメージのトラブルシューティング

マネージ コードの開発者のほとんどは、Microsoft .NET Framework で提供されている NGen ツールを使用した経験があるか、少なくとも聞いたことはあるはずです。実は、NGen は .NET Framework 1.0 以来提供されており、マイクロソフトはこれまでずっと .NET Framework のインストール時にフレームワークの中心となるアセンブリで使用してきました。

では、どうして今頃 NGen についての記事を書くのでしょうか? これには、2 つの答えがあります。近々リリースされる .NET Framework 2.0 では、NGen の機能が大幅に拡張され、パフォーマンスの高いマネージ アプリケーションを簡単かつ迅速に配置するための注目すべき数々の新機能が提供されています。この記事では、これらの新機能のいくつかについて紹介します。また、この記事では、どのような場合にどのようにすれば NGen を使用してアプリケーションのパフォーマンスを向上させることができるのかについても説明します。

まず NGen が行っていることの基本事項を簡単に見ていきましょう。ご存じのように、.NET アセンブリ中の Microsoft 中間言語 (MSIL) コードは、ジャストインタイム (JIT) コンパイラを使用して、コードを実行する直前にローカル コンピュータ用のネイティブ コードにコンパイルされます。これは一時的なコードであり、動作中のプロセスのメモリ空間に格納され、プロセスが終了すると OS によって再利用されます。したがって、新しいプロセスでこのコードが必要となった場合には、その都度再生成されます。

プリコンパイル (事前 JIT 処理) は、クライアント コンピュータ上で MSIL からネイティブ コードを生成することですが、実行時ではなく個別の手順として実行されます。事前 JIT 処理の出力は、プログラム実行時に JIT を動作させる代わりにネイティブ コードを使用できるよう維持されます。NGen は、共通言語ランタイム (CLR) での事前 JIT 技術の総称として使われ、とりわけネイティブ コードの作成と保存を行うコマンドライン ユーティリティを指します。NGen によって生成されたネイティブ コードは、"ネイティブ イメージ" ("NGen イメージ") と呼ばれる実際の Win32 PE ファイルに格納され、それがさらに "ネイティブ イメージ キャッシュ" ("NGen キャッシュ") に格納されます。"NGen キャッシュ" は、より広く NGen が格納するすべての情報に対しても使われます。これには、実際のネイティブ イメージだけでなく、どのアセンブリがネイティブ イメージを持っているかという情報も含まれます。

ネイティブ イメージを作成するとき、NGen は単純に JIT コンパイラをロードして実行し、そのアセンブリに対するネイティブ コードを生成します。このネイティブ コードは、ネイティブ イメージに保持され、ネイティブ イメージ キャッシュに格納されます。その後、アセンブリがマネージ アプリケーションによって使用されるときに、CLR ローダーがネイティブ イメージを検索、ロード、使用します。図 1 および図 2 は、これらのメカニズムに関係するすべての手順を示します (関連記事『ネイティブ イメージのトラブルシューティング』も参照してください)。

図 1 NGen の実行

図 1 NGen の実行

.NET Framework 1.0 と 1.1 の NGen 技術には、いくつかの重大な欠点がありました。あらゆるマネージ アセンブリには、唯一の依存物が mscorlib.dll に対するものだとしても、依存関係がありました。NGen イメージには、アセンブリに依存関係があるという仮定に基づいたコードとデータ構造が含まれていました。そのため、それらの依存物の変更に対して、非常に脆弱になっていました。依存物が変更されると、ネイティブ イメージ内のコードは無効となるためにランタイムはネイティブ イメージを使うことができず、代わりに旧方式である JIT コンパイルが使われます。これは、多くのアプリケーションで共用されるコンポーネントが更新される場合を考えると特に問題です。たとえば、System.dll が更新されると、そのコンピュータ上の多くのアプリケーションのネイティブ イメージが無効になります。

図 2 ネイティブ イメージはどのように使用されるか

図 2 ネイティブ イメージはどのように使用されるか

ページのトップへ


1. NGen 2.0 の登場

ネイティブ イメージに関する脆弱さの問題を解決することは、.NET Framework 2.0 の NGen 技術の主要な目標の 1 つでした。当初、考えられたアプローチがいくつかあります。最初のアプローチは、ネイティブ イメージから脆弱な相互依存関係を実際に取り除くことです。相互依存関係のほとんどは、アセンブリにまたがったメソッドのインライン化など、コンパイラの最適化に起因するため、この方法ではパフォーマンスに悪影響が出てしまい、トレードオフとしては許容できませんでした。採用された別のアプローチは、ネイティブ イメージとその依存関係の管理をできるだけ自動化し、共有コンポーネントが更新されても、アプリケーションのパフォーマンスが影響を受けるかどうかを開発者が心配しなくて済むようにすることです。

この目標を達成するため、NGen ツールのインターフェイスが変更され、利用モデルが新しくなりました。これまでは個別のアセンブリに対して NGen を起動していましたが、代わりにアプリケーションに対して NGen を起動するようになりました。これは、アプリケーション内の最上位のアセンブリに対して NGen を実行することで行われます (最上位のアセンブリとは、他のアセンブリからの依存関係がないアセンブリです)。NGen はアプリケーションの利用可能な依存物をすべて自動的に見つけ、すべての依存物にネイティブ イメージがあることも確認します (これは、アセンブリのマニフェストに記録されている静的な依存関係だけに適用されます。Assembly.Load などのメソッドで明示的にロードされるアセンブリは、NGen によって検出されません)。さらにNGen は、アプリケーションのすべての依存関係の記録を保持し、後から共有コンポーネントが更新された場合でも、更新によって影響を受けるすべてのアプリケーションを見つけ、必要に応じてネイティブ イメージを再生成できるようにします。

この追跡および更新のメカニズムに加えて、遅延コマンドもサポートされるようになりました。処理を完了するのにかなりの時間がかかるような大規模なアプリケーションで NGen を使う場合に、このコマンドをキューに格納すれば、新しい NGen サービスによるコマンド処理が可能だということです。NGen サービスは、バックグラウンドで動作する実際の Windows サービス プロセスで、コンピュータがアイドル状態になるのを待ってから、キュー内で待機している未処理のコンパイル ジョブを実行します。

NGen キューでは単純な優先度スキームがサポートされており、複数のジョブをキューに送る際に、アプリケーションがプリコンパイルされる順序を指定できます。バックグラウンドのコンパイルを行う際に、コンピュータがアイドル状態になるのを待つかどうかも、優先度によって決まります。このキュー機能は、共有コンポーネントの保守によって無効になったすべてのネイティブ イメージを更新する機能と組み合わせて使用すると特に便利です。この更新はかなり長い時間がかかる可能性があるため、NGen の更新コマンドの完了を待たずにコンポーネントの更新インストーラを終了したい場合があります。更新コマンドをキューに格納すれば、NGen サービスがバックグラウンドでコンパイルしてくれます。

主要な新機能以外にも多数の変更があり、これらを利用することで NGen がより便利になり、ネイティブ イメージが破棄されて JIT が実行されるような機会を減らすことができます。たとえば、NGen 2.0 で作成されたネイティブ イメージは、プロセス内のアプリケーション ドメイン間で共有できます。以前のバージョンでは、ネイティブ イメージは単一のアプリケーション ドメインでしか使用できませんでした。そのため、複数のアプリケーション ドメインを使うアプリケーションでは、NGen を使うことで得られるパフォーマンスの向上がかなり失われていました。この変更を実感できる主なアプリケーションの 1 つが ASP.NET アプリケーションで、以前はまさにこの理由によりネイティブ イメージを使用できませんでした。

もう 1 つの機能拡張が、ネイティブ イメージだけを使ったリフレクションが完全にサポートされたことです。これにより、ネイティブ イメージが使用できなかったもう 1 つのシナリオが解消されます。また、新しいジェネリック機能は、NGen イメージに対してほぼ 100 % サポートされます。JIT が使用される場合もわずかにありますが、そのようなケースはまれです。この機能はかなり特殊なものであり、実装の詳細については『Combining Generics, Precompilation and Sharing Between SoftwareBased Processes』 (英語)で説明されていますので、参照してください。また、.NET Framework 2.0 版の CLR では、特に NGen に焦点を当てた、規模は小さいながらも効果的なパフォーマンス向上のための仕組みがたくさん実装されています。NGen イメージの利用は、.NET Framework 2.0 を対象としたアプリケーションで最大のパフォーマンスを得るために最も有効な方法です。

ページのトップへ


2. NGen が適している場合

NGen が優れた機能を持っていることははっきりしましたが、いつ使用すべきなのかはどうすればわかるのでしょうか。NGen は、アプリケーションのためにいくつかのことを行います。NGen は、アプリケーションが使用するプライベート メモリ ページを削減し、これがシステム全体のメモリ使用量に良い影響を与えます。また、アプリケーションの起動時間も短縮されます。これらについて順に説明します。

マネージ アプリケーションが標準的な JIT のケースで動作する場合には、MSIL コードを含むアセンブリがメモリにロードされ、その MSIL コードに対して JIT コンパイラが起動されます。JIT コンパイラはプライベート メモリ (他のプロセスと共有できないメモリ) を割り当て、コンパイル済みのマシン語コードをそのメモリに書き込みます。ネイティブ イメージでは、JIT の場合と比較して、いくつかの方法でメモリ使用量が削減されます。まず、JIT コンパイラ自体がメモリにロードされません。JIT コンパイラは、ランタイムの他の部分とは別の DLL で実装され、必要な場合にだけランタイムによってロードされます。これにより、NGen を使用するアプリケーションでは、一定量のメモリ消費が削減されます。

2 番目に、ネイティブ イメージは他のすべての DLL と同じように CLR によってロードされます。これは、マシン語コードが格納されている実際のメモリ ページが、OS によって複数のプロセス間で共有できるということを意味します。これにより、そういった共有ネイティブ イメージの 1 つを使用するアプリケーションでは、すでにメモリにロードされているコード ページを使用できるため、そのプロセス用にコミットされる新規ページの数を減らすことができるという効果があります。ネイティブ イメージを使用するアプリケーションのすべてのインスタンスに対する、システム全体で必要なメモリの合計量もその分減少します。この恩恵は、.NET Framework のアセンブリや、Terminal Server 上で動作する複数のセッションで使われるアプリケーションなど、共有コンポーネントを利用する場合にだけ確認できます (図 3 を参照)。

図 3 共有メモリ

図 3 共有メモリ

特定の条件下では、NGen によってアプリケーションの起動時間も短縮されます。起動の際、モジュールがロードされ、メモリが割り当てられ、CLR がきわめて大量のコードを実行して自身を初期化し、場合によってはユーザー コードが JIT によりコンパイルされます。CLR パフォーマンス ラボの測定によれば、JIT コンパイルの代わりにネイティブ イメージを使用した場合、起動時間が大幅に短縮されることがわかっています。必ずしもすべてのアプリケーションで起動時間が短縮されるわけではなく、通常はウォーム スタートの場合に限定されます。ウォーム スタートとなるのは、マネージ プログラムが以前実行されて、ランタイムとネイティブ イメージのコードの多くがディスクからメモリにロード済みの場合です。コールドスタート (マネージ アプリケーションを、コンピュータを起動してから初めて起動する場合) では、起動時間のほとんどをディスク アクセスが占めるため、NGen の恩恵は受けられません。

このような利点があるにもかかわらず、NGen が常に最善の選択肢というわけではありません。パフォーマンスの問題は非常に複雑であり、コードやデータのアクセス パターン、コードとデータがファイル中でどのように配置されているか、モジュールの境界にまたがって何回呼び出されるか、必要なインフラストラクチャのうちどれだけが他のアプリケーションによってすでにロードされてメモリに存在しているかといった要素が関係します。こういったさまざまな要素のために分析は困難になりますが、重要なのは、アプリケーションを測定することで得られる実際の数値です。CLR のパフォーマンス設計者が力説するところによれば、最善策を見出す唯一の方法は、アプリケーションの測定を慎重に、さまざまな構成で繰り返し行うことです。

もう 1 つ説明しておくべきことがあります。厳密な名前のアセンブリを使用している場合、アセンブリがグローバル アセンブリ キャッシュ (GAC) の中にない限り、おそらく NGen を使用するメリットはありません。GAC 内にない厳密な名前のアセンブリに対しては、CLR ローダーは追加の検証を実行します。これにより、ネイティブ イメージ内のほぼすべてのページが読み込まれ、NGen を使用することで得られる起動時間の短縮の効果が通常すべて帳消しになってしまいます。

NGen のパフォーマンスについては、はるかに詳しい議論がオンラインで提供されており、単純な検索で簡単に見つかります。信頼できる参考文献は、『Improving .NET Application Performance and Scalability』(英語)第 5 章 (英語)です。

ページのトップへ


3. NGen のハードバインディング

.NET Framework 2.0 NGen のもう 1 つの新機能がハードバインディングです。JIT の方が NGen よりも優れている場合があります。これは、JIT がすべてのモジュールのコードとデータの仮想アドレスについてのより完全な情報を持ち、実際の仮想アドレスをターゲットとして使用して呼び出しやデータのロードを行うコードを作成できるためです。これに対して NGen は、他のアセンブリにある対象を呼び出したりデータにアクセスしたりする際に、コード内に従来のようにスタブを生成します。これらのスタブは、基本的にはランタイム内にある非常にコンパクトなルーチンであり、目的のメソッドやデータ構造体の実際の仮想アドレスを計算し、スタブの呼び出しを実際のターゲット仮想アドレスで上書きすることで、呼び出し側を "バックパッチ" します。このバックパッチにより、それまで読み込み専用だったコード ページが書き込み可能となるため、共有できなくなります。ハードバインディングを使用すれば、NGen は同様のコードを JIT として生成し、このバックパッチ (およびそれによる共有ページの減少) をなくすことができます。ただし、各ネイティブ イメージは非常に緊密に結びつけられたものとなってしまいます。この緊密な結びつきの結果、互いにハードバインドされたすべてのネイティブ イメージを同時にロードしなくてはならなくなります。起動時のパフォーマンスという面からは、すべてのアセンブリを起動時にロードしない、通常の状況下では、好ましくない影響となります。

ハードバインディングを使用するかどうかの最終的な判断は、ランタイムと NGen が行いますが、System.Runtime.CompilerServices.DependencyAttribute と System.Runtime.CompilerServices.DefaultDependencyAttribute を使うことで指示を与えることができます。前者の属性はアセンブリで設定し、特定の依存物がどの程度ロードされる見込みがあるかを NGen に伝えます。後者の属性は依存される側で設定し、その依存物がどの程度頻繁にロードされるかを指定します。親で DependencyAttribute が指定されない場合は、子の DefaultDependencyAttribute が使用されます。どちらの属性も、System.Runtime.CompilerServices.LoadHint 列挙値から値を取得し、Always、Default、Sometimes のいずれかとなります。Always を使用すると、ランタイムと NGen がその依存物に対してハードバインディングを使うという指示になります。それ以外の値は、どちらもハードバインディングを使用しないという意味になります。

これらの属性の使用を検討する場合は、目的とする結果が得られるよう、パフォーマンスの実験を行って慎重にデータを測定してください。これが正しい選択になるかどうかは、問題にしている個別のアプリケーションに大きく依存します。マイクロソフトでは、ベースとなる .NET Framework アセンブリのうち、mscorlib.dll などの少数のものにだけハードバインディング属性を使用しています。これらのアセンブリでは DefaultDependencyAttribute に LoadHint.Always が設定され、オーバーライドされない限り、他のアセンブリが既定でそのアセンブリにハードバインドされます。また、ハードバインディングでは、ベース アドレスを正しく取得することが重要です。これは、ベース アドレスが再配置されると、ハードバインドされたアセンブリは大きなコストを払うことになるためです。詳細については、関連記事『NGen イメージのベース アドレス』を参照してください。

ページのトップへ


4. 新しい NGen の使用方法

それでは、実際にこれらの新機能を使用する方法を見ていきましょう。この記事を執筆している時点では、ここで記載する情報は、.NET Framework Beta 2 のドキュメントよりも最新です。

NGen の機能には、コマンドライン ツール NGen.exe からしかアクセスできません。このツールを使用するための構文は、.NET Framework 2.0 で大きく変わりました。古い構文も引き続きサポートされていますが、新機能のすべてを利用できるわけではありません。新しい構文に完全に移行することをお勧めしますので、古いコマンドがどのように機能するかについては説明しません。

NGen のコマンド ラインでは、最初のパラメータとして必ずアクションを指定するようになりました。使用できるアクションを図 4 に示します。

図 4 NGen.exe のコマンドライン インターフェイス

NGEN INSTALL <ルート名> [<ビルド構成フラグ>] [<構成フラグ>] 
[<オプション フラグ>] [/queue:<優先度>]
NGEN UNINSTALL (<ルート名> | * ) [<ビルド構成フラグ>] [<構成フラグ>] 
[<オプション フラグ>]
NGEN DISPLAY [<ルート名>] [<ビルド構成フラグ>] [<構成フラグ>] 
[<オプション フラグ>] [/verbose]
NGEN UPDATE [ * ] [<オプション フラグ>] [/queue]
NGEN EXECUTEQUEUEDITEMS <優先度> [<オプション フラグ>]
NGEN QUEUE ( pause | continue ) [<オプション フラグ>]

シナリオ フラグ:
    /Debug
    /Profile
    /NoDependencies
構成フラグ:
    /AppBase:<パス>
    /ExeConfig:<実行ファイルへの絶対パス>
オプション フラグ:
    /nologo
    /silent
    /verbose
優先度:
    1、2、または 3
ルート名:
    ファイル名または GAC 内のアセンブリのアセンブリ表示名

install アクションは、ルートと呼ばれる最上位アセンブリのネイティブ イメージを NGen キャッシュに追加します。NGen はルートの依存関係を自動的に見つけ、依存するアセンブリに対するネイティブ イメージもキャッシュに追加します。ルート AccountingApp.exe を NGen キャッシュに追加するために必要なコマンド ラインは、次のようになります。

ngen install AccountingApp.exe

AccountingApp.exe の依存関係を調べる際、NGen は CLR が実行時に使用するのと同じアセンブリプローブ ロジックを使用します。既定では、NGen は AccountingApp.exe があるディレクトリを AppBase として使用します。つまり、アセンブリのプローブがこのディレクトリから開始されます。以前のバージョンの NGen では、AppBase はカレント ディレクトリに設定されていましたので、この点が異なります。ほとんどの場合、この新しい動作は期待どおりに正しく機能します。

しかし、NGen が依存物を見つけるために特別な助けが必要な場合があります。/appbase スイッチを使用すると、アセンブリがあるディレクトリから、指定する任意のディレクトリに AppBase をオーバーライドできます。/execonfig スイッチは、DLL アセンブリをネイティブ イメージ キャッシュに追加する際に使用します。このスイッチを指定すると、構成ファイルが関連付けられている実行ファイル名を指定できます。NGen は、その実行ファイルの構成ファイル内の情報を利用して、追加する DLL の依存物を検索します。

GAC 中のアセンブリは、NGen キャッシュにもインストールできます。そのために必要な構文では、ファイル名の代わりにアセンブリの表示名を使用します。

ngen install "payroll, Version=1.0.1.2, Culture=neutral,
PublicKeyToken=ab7e26f746a3c28e, processorArchitecture=MSIL"

NGen はまた、さまざまなビルド構成フラグをサポートしており、install アクションで指定できます。その中で最も便利なのが、debug と profiling です。debug 構成フラグは、そのルートに対する、デバッガで使用できるネイティブ イメージのセットを生成するよう NGen に指示します。既定のネイティブ イメージを使ってデバッガ配下でアプリケーションを起動しようとすると、ランタイムにより JIT が使われて、デバッグ可能なコードが生成されます。NGen に debug フラグを渡すことで、ネイティブ イメージ内にデバッグ可能なコードが生成されます。これにより、プログラムをデバッガ内で起動できると共に、そのプログラムでネイティブ イメージのコードを使用できます。ネイティブ イメージを使用して動作しているプログラムにマネージ デバッガでアタッチしたり、呼び出し履歴やローカル、慣れ親しんだ他のあらゆるデバッグ手法を参照したりするには、これが唯一の方法です。同様に、profiling スイッチは、プロファイラで使用できるネイティブ イメージを生成するように NGen に指示します。これらのスイッチを使用すると、単一のルートに対して、NGen キャッシュ内に複数のビルド構成を作成できます。たとえば、次の 2 つのコマンドを実行します。

ngen install AccountingApp.exe
ngen install AccountingApp.exe /debug

これら 2 つのコマンドを実行すると、ネイティブ イメージ キャッシュの中に、AccountingApp に対する既定のネイティブ イメージとデバッグ用のネイティブ イメージの両方が作成されます。

uninstall コマンドは install コマンドと非常に良く似ていますが、名前から明らかなように、ルートとその依存物を NGen キャッシュから削除します。NGen によってキャッシュ内のルートの依存関係が追跡されるため、他のルートが依存している共有コンポーネントのネイティブ イメージを誤って削除してしまうのではないかという心配は不要です。NGen は、インストールされているルートで共有コンポーネントに依存しているものがある限り、その共有コンポーネントのネイティブ イメージをキャッシュ内に残します。install と同様に、uninstall もビルド構成フラグ (debug、profile) とアセンブリのプローブ フラグ (appbase、execonfig) をサポートします。これにより、ルートのネイティブ イメージを削除する際に、既定の構成についてのイメージを削除せずに、デバッグ構成のイメージを選択的に削除できます。ただし、フラグを指定せずに uninstall を実行すると、指定したルートについて、すべての構成のアンインストールを NGen に指示することになるため、注意してください。以下に uninstall コマンドの例を示します。

ngen uninstall Payroll.dll

ngen uninstall "payroll, Version=1.0.1.2, Culture=neutral, 
    PublicKeyToken=ab7e26f746a3c28e, processorArchitecture=MSIL"

ngen uninstall "payroll, Version=1.0.1.2, Culture=neutral, 
    PublicKeyToken=ab7e26f746a3c28e, processorArchitecture=MSIL" /debug

ngen uninstall payroll

最初のコマンドは、カレント ディレクトリにあるルート、Payroll.dll を削除します。これには、そのルートに関するすべてのビルド構成が含まれます。2 番目のコマンドは、指定した表示名に一致するアセンブリのルートを GAC から削除します。このコマンドも、そのルートに関するすべてのビルド構成を削除します。3 番目のコマンドは、ルート "payroll, Version=1.0.1.2, Culture=neutral, PublicKeyToken=ab7e26f746a3c28e, processorArchitecture=MSIL" に関するデバッグ構成だけを削除します。最後のコマンドは、uninstall の最も単純なコマンドで、これまでの例に登場したすべてのルートを含め、payroll という簡易名を持つすべてのルートが削除されます。

display コマンドを使用すると、ネイティブ イメージ キャッシュの中を覗き見ることができます。次のように、引数なしで使用できます。

ngen display

このコマンドでは、キャッシュ内のすべてのルートに関する情報が表示されます。また、アセンブリ名の全部または一部を指定することもできます。

ngen display payroll.dll
ngen display payroll

どちらのコマンドでも、ルート Payroll.dll の情報だけが表示されます。最初のコマンドは、Payroll.dll への絶対パスを指定するか、Payroll.dll と同じディレクトリで実行する場合にだけ動作します。2 番目のコマンドでは、"payroll, Version=1.0.1.2, Culture=neutral, PublicKeyToken=ab7e26f746a3c28e, processorArchitecture=MSIL" という名前のルートの情報も表示されます。出力には 2 つのセクションがあります。最初のセクションはすべてのルート アセンブリの一覧であり、次のセクションはシステムのキャッシュ内にあるネイティブ イメージのフラットな一覧です。/verbose スイッチを指定すると、状態に関するより詳しい情報が表示されますが、内容についてはここでは説明しません。次のような構文を使用すると、指定したアセンブリのネイティブ イメージに依存するルートの一覧も表示されます。

ngen display payroll

このコマンドでは、簡易アセンブリ名が payroll であるすべてのルート、簡易アセンブリ名が payroll であるアセンブリに依存するすべてのルート、簡易アセンブリ名が payroll であるアセンブリに対するすべてのネイティブ イメージが表示されます。これは、共有コンポーネントを更新した後でいくつのルートを再生成する必要があるかを調べるのに便利です。

NGen の次のアクションは "update" です。update アクションは、これまで説明してきたように引数を受け取らず、単純に、キャッシュ内のすべてのルートの依存関係を再計算して、最新でないものに対して新しいネイティブ イメージを生成します。update は主に、システム上で共有コンポーネントを更新した後に使用するよう意図されています。 コマンドは次のように単純です。

ngen update

キャッシュ内に大量のルートがある場合は、NGen update コマンドが完了するまでに長い時間がかかることがあります。主にこの理由のため、.NET Framework 2.0 では遅延 NGen 機能が追加されました。

遅延 NGen を使うと、コマンドライン ツールから NGen に対してコマンドを発行し、バックグラウンドでサービスを使用して何らかのアクションを実行するよう指示できます。そのためには、install アクションと update アクションの両方でサポートされている /queue スイッチを使用します。たとえば次のようになります。

ngen update /queue

これにより、NGen によって完全な update アクションが実行されますが、コンピュータがアイドル状態になるまで待ってからすべての作業が実行されます。コマンド自体はすぐに終了するため、呼び出し側のスクリプトは NGen の作業がまだ完了していなくても終了できます。遅延コマンドは、アイドル時間検知規則に従って NGen サービス プロセスにより実行されます。この規則については、最終的な製品のリリース前に微調整されるため、あまり詳しく説明することは避けたいと思います。ただし一般に、サービスは、タイムアウト期間にユーザー入力がなかったことを検出すると、コンピュータがアイドル状態にあることを宣言して、必要なコンパイルを行う通常の優先度のワーカー プロセスを起動し始めます。

install コマンドにはそれ以外の機能もあります。/queue スイッチでは、遅延 install コマンドを実行する際の優先度を示す 1 ~ 3 の引数を指定できます。 優先度 1 と 2 のコマンドは、バックグラウンドでサービスにより処理されますが、アイドル状態になるのを待つことはしません。代わりに、ワーカー プロセスがサービスによってすぐに起動され、コンパイル作業が開始されます。優先度 3 のコマンドは、更新コマンドと同じアイドル時間規則に従います。実際、更新コマンドでは、常に優先度 3 が暗黙的に設定されます。 install コマンドは次のようになります。

ngen install AccountingApp.exe /queue:2

このコマンドは、AccountingApp.exe の新しいルートをキャッシュに追加します。その後、AccountingApp.exe とそのすべての依存物のネイティブ イメージをすぐにコンパイル開始するようサービスに指示します。

優先度 1 と優先度 2 の違いは、優先度 1 のコマンドは、優先度 2 以降のコマンドよりも前に完了することが保証されるという点です。その点以外は優先度 1 と 2 は同じで、どちらもコンピュータがアイドル状態になるのを待たずに、サービスによるバックグラウンドでのコンパイルがすぐに開始されます。優先度は、executequeueditems アクションと共に使用するケースでも有用です。このアクションを指定すると、キュー内のコマンドがすぐに実行されます。これは同期型のコマンドで、指定されたすべての作業が完了するまではコマンドが終了しません。executequeueditems はオプションの引数として優先度の数値を受け取り、キュー内にある、優先度が指定レベル以下のコマンドだけを処理します。 たとえば、

ngen executequeueditems 2

を実行すると、キュー内の優先度 1 と 2 のすべてのコマンドが同期的に実行されます。また、おわかりのことと思いますが、

ngen executequeueditems 3

を実行すると、キュー内のすべてのコマンドが同期的に実行されます。executequeueditems の既定の優先度は 3 です。

注意すべき点は、同期型のコマンドは、サービスを一切介さずに実行されることです。つまり、executequeueditems を含め、/queue スイッチを指定しない NGen コマンドは、ngen.exe とそれによって起動されるワーカー プロセスとで完全に実行されます。これらのコマンドは、サービスが無効になっていても動作し、コマンドを実行したからといってサービスは開始されません。サービス自体は Windows サービス プロセスで、通常の状態では完全にシャットダウンされています。ngen.exe を通じてコマンドがサービスのキューに格納されると、サービスが起動され、キュー内に処理対象のコマンドがなくなるまで、リブートしても起動されたままになります。処理すべきコマンドがなくなると、サービスは自身がリブート時に起動されないよう設定し、シャットダウンします。その後サービスは、別の NGen コマンドがキューに格納されるまで、シャットダウンされまままになります。

最後に説明するアクションは、一般的な使用シナリオを検討する際の橋渡しとなるものです。queue アクション (/queue スイッチと混同しないでください) は、サービスの動作を制御するための手段です。queue アクションでは、pause、continue、status の 3 つの引数がサポートされています。コンピュータへのファイルのコピーを開始する前に、すべてのインストーラで queue pause コマンドを実行することをお勧めします。

ngen queue pause

これにより、インストーラがファイルをコピーし、GAC にアセンブリを追加してプリコンパイルしている間は、サービスがバックグラウンドで何も実行しないことが保証されます。この目的は、インストーラとサービスが同じリソースにアクセスしようとして競合しないようにすることです。処理を完了すると、インストーラは次のコマンドを実行します。

ngen queue continue

これによりサービスは、キューに格納されたコマンドの処理を再開するか、処理がなければシャットダウン状態に戻ることができます。ngen queue コマンドに対して status 引数を指定することで、サービス プロセスに関する診断情報がコンソールに出力されます。

ページのトップへ


5. すべてを組み合わせた例

シナリオに従ってすべてを組み合わせ、裏で何か起きるかを display コマンドを使って調べてみます。このシナリオでは、小規模な業務で使用されるツール一式の配置パッケージを作成します。中心となるツールは Inventory.exe、TaxWizards.exe、および Books.exe です。これらの中心となるツールに加え、パッケージはこのスイート内のすべてのアプリケーションで共有される Payroll.dll という DLL と、サードパーティの開発ツールキットのコンポーネント Chart.dll を配置します。ただし、後者はユーザーのシステム上で他のアプリケーションからも使用される可能性があります。インストールは比較的短時間で終わるため、インストール時にすべてのコンポーネントに対して NGen を実行することはできません。しかし、アプリケーションは比較的大きく、NGen を使うことで起動時間がかなり短縮されることがわかっています。そこで、最も重要なアプリケーションである Books.exe の事前 JIT 処理をインストール中に終え、コマンドをキューに格納して、残りのアプリケーションはインストール後にプリコンパイルすることにします。作為的な部分も多少ありますが、このシナリオ例は、サポートされる主要な作業のすべてを広くカバーしています。

このパッケージのインストール スクリプトは次のようになります。

ngen queue pause
... (アプリケーションのディレクトリにすべてのファイルを xcopy)
... (Payroll.dll を GAC にインストール)
... (Charts.dll を GAC にインストール)
... (アプリケーションのレジストリ キーを追加)
ngen install Books.exe /queue:1
ngen install Inventory.exe /queue:2
ngen install TaxWizards.exe /queue:2
ngen update /queue
ngen executequeueditems 1
ngen queue continue

まず、スクリプトはサービスを一次停止してリソースが競合しないようにします。次に、すべてのファイルをコピーし、共有コンポーネントを GAC に追加し、必要なレジストリ キーを設定します。それから install コマンドをキューに格納し、それを使用して 3 つの最上位アセンブリをネイティブ イメージ キャッシュのルートとしてインストールします。その後、update コマンドをキューに格納します。この目的は、共有コンポーネント Chart.dll に依存するすべてのネイティブ イメージが、必要に応じて更新されるようにすることです。これは、Chart.dll に依存するアセンブリが、現在インストールされている Chart.dll に自動的にアップグレードされる場合にだけ必要です。これは、Chart.dll への更新がバージョン番号のビルド リビジョン部分だけの場合 (たとえば 1.0.0.1 から 1.0.0.3 への更新) や、このコンポーネントに影響するバインド ポリシーが更新された場合に当てはまります。いずれにしても、GAC にコンポーネントをインストールする場合には、必ず NGen の update コマンドを実行しておくのが確実です。

スクリプトでは、次に executequeueditems コマンドを実行しています。このコマンドの引数は 1 であり、キューに格納されている優先度 1 のコマンドだけを強制的に実行するよう指示しています。最後に、"queue continue" コマンドで NGen のキューを再度アクティブにしています。スクリプトの最後のコマンドが完了すると、キューに登録されている優先度 2 のコマンドの処理がサービスによって自動的に開始されます。前述のとおり、優先度 2 のコマンドはアイドル状態になるのを待つ必要がなく、すぐにバックグラウンドで実行されます。その後、コンピュータがアイドルになったことをサービスが検出すると、その時点でキューに登録されている update コマンド (暗黙的に優先度が 3 に設定されている) が実行されます。

スクリプトを実行する前のキャッシュの状態を見てみましょう。

>ngen display
Microsoft (R) CLR Native Image Generator - Version 2.0.41203.00
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Roots:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86ret\mscorlib.dll

Native Images:

mscorlib, Version=2.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089

上記例の出力では、関係のある情報だけを残してあとは省略しています。この出力では、1 つのルート mscorlib.dll だけがキャッシュに追加されています。"Native Image:" という見出しの下に、ルート mscorlib に対する 1 つのネイティブ イメージだけが表示されています。では図 5 を見てください。これは、スクリプト内の 3 つの install コマンドすべてを実行した後の display コマンドの出力です。4 つのルート (以前からある mscorlib のルートと、各 ngen install コマンドのそれぞれでインストールされた新しいルート) があるのがわかります。また、新しいルートには "StatusPending" というタグが付いているのがわかります。これは、これら新しいルートのインストールはキューに登録されただけであり、実際の作業はまだ行われていないことを意味します。このことは、ネイティブ イメージが 1 つ (mscorlib のネイティブ イメージ) しか表示されていないという事実からもわかります。

図 5 display コマンドの出力

>ngen display
Microsoft (R) CLR Native Image Generator - Version 2.0.41203.00
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Roots:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86ret\mscorlib.dll
D:\Scratch\SBApp\Books.exe (StatusPending)
D:\Scratch\SBApp\Inventory.exe (StatusPending)
D:\Scratch\SBApp\TaxWizards.exe (StatusPending)

Native Images:

mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

図 6 に、キューに登録された update コマンドが実行された後の出力を示します。この出力と図 5 との違いは、今度は mscorlib.dll のルートも "StatusPending" とマークされている点です。ここに、update コマンドがどのように実装されているかを垣間見ることができます。update では、まずすべてのルートがマークされます。このマークによって、サービスはそのルートを再度調べ、依存関係のリストを再度生成し、まだキャッシュ内に有効なネイティブ イメージが存在しないすべてのアセンブリのネイティブ イメージを生成します。これで、mscorlib.dll のルートに対する新しい状態が明らかになります。また、mscorlib.dll のネイティブ イメージがまだ残っていることもわかります。update は、無効であると確定されるまではネイティブ イメージを削除しません。サービスによってこの update が処理されると、キャッシュ内に mscorlib.dll に対する有効なネイティブ イメージがすでに存在することがわかり、新しいネイティブ イメージは再生成されません。

図 6 display コマンドの出力例 2

>ngen display
Microsoft (R) CLR Native Image Generator - Version 2.0.41203.00
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Roots:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86ret\mscorlib.dll (StatusPending)
D:\Scratch\SBApp\Books.exe (StatusPending)
D:\Scratch\SBApp\Inventory.exe (StatusPending)
D:\Scratch\SBApp\TaxWizards.exe (StatusPending)

Native Images:

mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

executequeueditems コマンドを実行した後で再度キャッシュを表示した結果を図 7 に示します。Books.exe に対するルートには "StatusPending" タグが付いていないため、完了したことがわかります。追加した他のルートはまだ保留中です。今度は、Books.exe とその依存物 Chart.dll および Payroll.dll のネイティブ イメージが作成されています。スクリプト内の最後のコマンドによって、バックグラウンドのコンパイルを再開するようにサービスに指示されます。前述のように、サービスは、キューに登録されているコマンドのうち、優先度の高いものから処理します。そのため、Inventory.exe と TaxWizards.exe に対する、キューに登録された優先度 2 の install が最初に実行されます (アイドル状態になるのを待つことはしません)。キューに登録された update コマンドは最後に (アイドル時に) 実行されます。それまでに他のコマンドが実行されていなければ、残っている作業は、ルート mscorlib.dll の有効なネイティブ イメージがあることを確認するだけです。すべての作業を終えた後のキャッシュの最終的な状態を図 8 に示します。

図 7 キャッシュ

>ngen display
Microsoft (R) CLR Native Image Generator - Version 2.0.41203.00
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Roots:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86ret\mscorlib.dll (StatusPending)
D:\Scratch\SBApp\Books.exe
D:\Scratch\SBApp\Inventory.exe (StatusPending)
D:\Scratch\SBApp\TaxWizards.exe (StatusPending)

Native Images:

Books, Version=1.0.1.1, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a
mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Payroll, Version=1.0.1.1, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a
Chart, Version=3.1.0.0, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a

図 8 最終的なキャッシュの状態

>ngen display
Microsoft (R) CLR Native Image Generator - Version 2.0.41203.00
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Roots:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86ret\mscorlib.dll
D:\Scratch\SBApp\Books.exe
D:\Scratch\SBApp\Inventory.exe
D:\Scratch\SBApp\TaxWizards.exe

Native Images:

Books, Version=1.0.1.1, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a
mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Payroll, Version=1.0.1.1, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a
Chart, Version=3.1.0.0, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a
Inventory, Version=1.0.1.1, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a
TaxWizards, Version=1.0.1.1, Culture=neutral, PublicKeyToken=3a0f3302b4d4c63a

ページのトップへ


6. シャットダウン

サービスは、インストール スクリプトの中でキューに登録されたすべての作業を終えると、それ以上リソースを消費しないために自分自身をシャットダウンします。再度起動されるのは、NGen を通じて別のコマンドがキューに登録された場合だけです。サービスをできるだけ目に付かず控えめなままにしておくことが、この機能の設計目標の 1 つです。全体としては、実際、事前 JIT 処理によるパフォーマンスの恩恵を得るための、堅牢で手動操作が不要なインフラストラクチャを提供する、というのが CLR での NGen 技術の目標です。.NET Framework 2.0 の NGen 機能は、この目標にかなり近づきました。

これで、.NET Framework 2.0 で可能になる NGen の新機能について理解し、皆さんのマネージ アプリケーションにこの技術を受け入れる準備ができたことと思います。NGen が皆さんのアプリケーションや顧客のシナリオで有効かどうかを判断するためには、アプリケーションを慎重に測定し、収集したデータに基づいて判断することを忘れないでください。近くリリースされる .NET Framework 2.0 で拡張された機能とともに、NGen は、マネージ アプリケーションに必要なパフォーマンス エッジを得るための最適な手段です。

ページのトップへ


7. 補足記事: NGen イメージのベース アドレス

NGen イメージの正しいベース アドレスを取得することは、少なくともアンマネージ コード モジュールと同様に重要です。NGen イメージが優先ベース アドレスにないと、CLR は、ネイティブ イメージをその新しいアドレスで置き換えるための特別なコードを実行する必要があります。ハードバインドされたネイティブ イメージは、置き換えで必要な処理が多く、再配置に対して敏感なため、この影響がより顕著になります。ハードバインドされた依存物が再配置されると、そのアセンブリのすべての呼び出し元もロード時に修正しなくてはならなくなります。ネイティブ イメージのベース アドレス設定は、言語固有のコンパイラで生成されたアセンブリ上にベース アドレスを設定するという方法で行います。C#、Visual Basic、C++ の各コンパイラには、生成するアセンブリのベース アドレスを制御するためのオプションがあります。また、MSIL 内で ".imagebase" ディレクティブを使用すると、指定されたベース アドレスを持つアセンブリが ILASM によって生成されます。NGen は、アセンブリが実行ファイル (.exe) である場合を除いて、このネイティブ イメージのベース アドレスを維持します。実行ファイルの場合、ネイティブ イメージ上のベース アドレスは、実行ファイル自身が優先ベース アドレスにロードされるような十分なオフセットに配置されます。ネイティブ イメージは実際の Win32 PE ファイルであるため、dumpbin.exe などのツールを使って、キャッシュ内のネイティブ イメージの優先ベース アドレスを表示できます。

ページのトップへ


8. 補足記事: ネイティブ イメージのトラブルシューティング

NGen によって生成されるネイティブ イメージは、CLR が管理するプライベート キャッシュに格納され、CLR によって自動的に使用されます。全体的に、システムは完全なブラック ボックスで、"単に動作するとだけ仮定されて" います。しかし、ネイティブ イメージが破棄されるような境界条件にヒットしていないことを確実にするため、ネイティブ イメージが実際にアプリケーションで使用されていることを確認できると便利です。プロセスでネイティブ イメージが使用されているかどうかは、デバッガをアタッチし、ネイティブ イメージの PE ファイルがプロセスにロードされているかどうかを調べればわかります。このテストを行うには、デバッガをアタッチする必要があります。デバッグ用のネイティブ イメージを作成しない限り、デバッガ内でプログラムを起動しても、ネイティブ イメージはまったくロードされません。ネイティブ イメージ ファイルの名前は、元のアセンブリと同じで、ファイル名と拡張子の間に ".ni." が追加されます。たとえば、mscorlib.dll のネイティブ イメージは "mscorlib.ni.dll" という名前になります。ネイティブ イメージがどのように使用されているかを調べるもう 1 つの方法は、.NET Framework SDK で提供されている fuslogvw.exe ツールを使用することです。このツールには、ネイティブ イメージのバインド情報をログに記録するモードがあり、メイン パネル上のラジオ ボタンで有効になります。fuslogvw には、ネイティブ イメージが使用されなかった場合に、その理由に関する情報をログに記録するという便利な機能もあります。


Reid Wilkes はマイクロソフトの社員です。彼は、共通言語ランタイムを担当する品質保証チームに所属し、主に NGen 技術を扱っています。


この記事は、MSDN マガジン - 2005 年 4 月号からの翻訳です。

ページのトップへ