.NET FrameworkのRun-Time テクノロジのパフォーマンスに関する考慮事項
エマニュエル・シャンツァー
Microsoft Corporation
2001 年 8 月
概要: この記事には、マネージド 世界で働いているさまざまなテクノロジの調査と、それらがパフォーマンスに与える影響に関する技術的な説明が含まれています。 ガベージ コレクション、JIT、リモート処理、ValueTypes、セキュリティなどの動作について説明します。 (27ページ印刷)
内容
概要
ガベージ コレクション
スレッド プール
The JIT
AppDomain
セキュリティ
リモート処理
ValueTypes
その他のリソース
付録: サーバーのランタイムのホスティング
概要
.NET ランタイムでは、セキュリティ、開発の容易さ、パフォーマンスを目的とした高度なテクノロジがいくつか導入されています。 開発者は、各テクノロジを理解し、コードで効果的に使用することが重要です。 実行時に提供される高度なツールを使用すると、堅牢なアプリケーションを簡単に構築できますが、そのアプリケーションを高速に起動させるのは、開発者の責任です (常にそうでした)。
このホワイト ペーパーでは、.NET で動作するテクノロジをより深く理解し、コードの速度を調整するのに役立ちます。 注: これはスペック シートではありません。 そこに既に多くの固体技術情報があります。 ここでの目標は、パフォーマンスに対して強い傾きを持つ情報を提供することです。また、あなたが持っている技術的な質問に答えるわけではありません。 ここで求める回答が見つからない場合は、MSDN オンライン ライブラリでさらに詳しく調べることをお勧めします。
次のテクノロジについて取り上げ、その目的とパフォーマンスに影響を与える理由の概要を説明します。 次に、下位レベルの実装の詳細を詳しく調べ、サンプル コードを使用して、各テクノロジから速度を上げる方法を示します。
ガベージ コレクション
基本操作
ガベージ コレクション (GC) を使用すると、使用されなくなったオブジェクトのメモリを解放することで、プログラマは一般的でデバッグが困難なエラーから解放されます。 オブジェクトの有効期間に続く一般的なパスは、マネージド コードとネイティブ コードの両方で次のとおりです。
Foo a = new Foo(); // Allocate memory for the object and initialize
...a... // Use the object
delete a; // Tear down the state of the object, clean up
// and free the memory for that object
ネイティブ コードでは、これらすべてのことを自分で行う必要があります。 割り当てフェーズまたはクリーンアップ フェーズが見つからないと、デバッグが困難なまったく予測できない動作が発生し、解放オブジェクトを忘れた場合にメモリ リークが発生する可能性があります。 共通言語ランタイム (CLR) でのメモリ割り当てのパスは、先ほど説明したパスに非常に近いです。 GC 固有の情報を追加すると、非常によく似た内容になります。
Foo a = new Foo(); // Allocate memory for the object and initialize
...a... // Use the object (it is strongly reachable)
a = null; // A becomes unreachable (out of scope, nulled, etc)
// Eventually a collection occurs, and a's resources
// are torn down and the memory is freed
オブジェクトを解放できるようになるまで、両方の世界で同じ手順が実行されます。 ネイティブ コードでは、オブジェクトの使用が完了したら、オブジェクトを解放することを忘れないでください。 マネージド コードでは、オブジェクトに到達できなくなったら、GC はそれを収集できます。 もちろん、リソースを解放するために特別な注意が必要な場合 (ソケットを閉じるなど)、GC で正しく閉じるためのヘルプが必要な場合があります。 解放する前にリソースをクリーンするために前に記述したコードは、Dispose() メソッドと Finalize() メソッドの形式で適用されます。 この 2 つの違いについては、後で説明します。
周りのリソースへのポインターを保持する場合、GC には、将来使用する予定があるかどうかを知る方法はありません。 つまり、オブジェクトを明示的に解放するためにネイティブ コードで使用したすべてのルールは引き続き適用されますが、ほとんどの場合、GC ですべての処理が行われます。 100% の時間のメモリ管理について心配する代わりに、5% 程度の時間について心配するだけで済みます。
CLR ガベージ コレクターは、世代別のマークとコンパクトなコレクターです。 それは優秀な性能を達成することを可能にするいくつかの原則に従う。 まず、有効期間が短いオブジェクトは小さく、頻繁にアクセスされるという概念があります。 GC は、割り当てグラフを複数のサブグラフ ( 世代と呼ばれます) に分割します。これにより、収集にできるだけ少ない時間を費やすことができます*.* Gen 0 には、頻繁に使用される若いオブジェクトが含まれています。 また、これは最小になる傾向があり、収集には約 10 ミリ秒かかります。 GC は、このコレクション中に他の世代を無視できるため、はるかに高いパフォーマンスを提供します。 G1 と G2 は、より大きく古いオブジェクト用であり、収集頻度が低くなります。 G1 コレクションが発生すると、G0 も収集されます。 G2 コレクションは完全なコレクションであり、GC がグラフ全体を走査する唯一の時間です。 また、CPU キャッシュをインテリジェントに使用します。これにより、実行される特定のプロセッサのメモリ サブシステムをチューニングできます。 これは、ネイティブ割り当てでは簡単に使用できない最適化であり、アプリケーションのパフォーマンス向上に役立ちます。
コレクションはいつ発生しますか?
時間割り当てが行われると、GC はコレクションが必要かどうかを確認します。 コレクションのサイズ、残りのメモリの量、および各世代のサイズを調べます。その後、ヒューリスティックを使用して決定を行います。 コレクションが発生するまで、オブジェクトの割り当ての速度は、通常、C または C++ よりも高速 (または高速) になります。
コレクションが発生するとどうなりますか?
ガベージ コレクターがコレクション中に実行する手順を見てみましょう。 GC は、GC ヒープを指すルートの一覧を保持します。 オブジェクトがライブの場合、ヒープ内の位置にルートがあります。 ヒープ内のオブジェクトは互いを指すこともできます。 このポインターのグラフは、GC が領域を解放するために検索する必要があるグラフです。 イベントの順序は次のとおりです。
マネージド ヒープは、すべての割り当て領域を連続したブロックに保持し、このブロックが要求された量より小さい場合は、GC が呼び出されます。
GC は、各ルートと後続のすべてのポインターに従い、到達 できない オブジェクトの一覧を維持します。
ルートから到達できないすべてのオブジェクトは収集可能と見なされ、コレクション用にマークされます。
図 1. コレクションの前: ルートからすべてのブロックに到達できるわけではないことに注意してください。
到達可能性グラフからオブジェクトを削除すると、ほとんどのオブジェクトが収集可能になります。 ただし、一部のリソースは特別に処理する必要があります。 オブジェクトを定義するときは、 Dispose() メソッドまたは Finalize() メソッド (またはその両方 ) を記述するオプションがあります。 この 2 つの違いと、後で使用するタイミングについて説明します。
コレクションの最後の手順は圧縮フェーズです。 使用中のすべてのオブジェクトが連続したブロックに移動され、すべてのポインターとルートが更新されます。
ライブ オブジェクトを圧縮し、空き領域の開始アドレスを更新することで、GC は、すべての空き領域が連続していることを維持します。 オブジェクトを割り当てるのに十分な領域がある場合、GC はプログラムに制御を返します。 そうでない場合は、 が発生します
OutOfMemoryException
。図 2. コレクション後: 到達可能なブロックが圧縮されました。 より多くの空き領域!
メモリ管理の詳細については、「Microsoft Windows 用プログラミング アプリケーション by Jeffrey Richter 」(Microsoft Press、1999) の第 3 章を参照してください。
オブジェクトのクリーンアップ
一部のオブジェクトでは、リソースを返す前に特別な処理が必要です。 このようなリソースの例としては、ファイル、ネットワーク ソケット、またはデータベース接続があります。 これらのリソースを正常に閉じる必要があるため、ヒープ上のメモリを解放するだけでは十分ではありません。 オブジェクトのクリーンアップを実行するには、 Dispose() メソッド、 Finalize() メソッド、またはその両方を記述します。
Finalize() メソッド:
- GC によって呼び出されます
- 任意の順序で、または予測可能な時間に呼び出される保証はありません
- 呼び出された後、 次 の GC の後にメモリを解放します
- すべての子オブジェクトを次の GC まで保持する
Dispose() メソッド:
- プログラマによって呼び出されます
- プログラマが順序付けしてスケジュールする
- メソッドの完了時にリソースを返します
マネージド リソースのみを保持するマネージド オブジェクトでは、これらのメソッドは必要ありません。 プログラムでは、おそらくいくつかの複雑なリソースしか使用しません。また、それらのリソースが何であるか、必要なときに知っている可能性があります。 これらの両方がわかっている場合は、クリーンアップを手動で行うことができるため、ファイナライザーに依存する理由はありません。 これを行う理由はいくつかありますが、これらはすべて ファイナライザー キューと関係があります。
GC では、ファイナライザーを持つオブジェクトが収集可能とマークされると、そのオブジェクトとそのオブジェクトが指すすべてのオブジェクトが特別なキューに配置されます。 別のスレッドがこのキューを下に進み、キュー内の各項目の Finalize() メソッドを呼び出します。 プログラマは、このスレッド、またはキューに配置された項目の順序を制御しません。 GC は、キュー内のオブジェクトを確定することなく、プログラムに制御を返す場合があります。 これらのオブジェクトはメモリ内に残り、長時間キューに収まる可能性があります。 最終処理の呼び出しは自動的に実行され、呼び出し自体による直接的なパフォーマンスへの影響はありません。 ただし、ファイナライズのための非決定論的モデルは 、他 の間接的な結果をもたらす可能性があります。
- 特定の時間に解放する必要があるリソースがあるシナリオでは、ファイナライザーを使用して制御できなくなります。 たとえば、ファイルを開き、セキュリティ上の理由から閉じる必要があるとします。 オブジェクトを null に設定し、GC をすぐに強制的に実行した場合でも、 ファイルは Finalize() メソッドが呼び出されるまで開いたままになり、これがいつ発生するのか分かりません。
- 特定の順序で破棄を必要とする N 個のオブジェクトが正しく処理されない場合があります。
- 巨大なオブジェクトとその子は、メモリを過剰に占有し、追加のコレクションを必要とし、パフォーマンスを低下させる可能性があります。 これらのオブジェクトは、長い間収集されない場合があります。
- 確定する小さなオブジェクトには、いつでも解放できる大きなリソースへのポインターが含まれる場合があります。 これらのオブジェクトは、ファイナライズするオブジェクトが処理されるまで解放されず、不要なメモリ負荷が発生し、頻繁にコレクションが強制されます。
図 3 の状態図は、オブジェクトが最終処理または破棄の観点から取ることができるさまざまなパスを示しています。
図 3: オブジェクトが受け取ることができる破棄パスと最終処理パス
ご覧のように、ファイナライズでは、オブジェクトの有効期間にいくつかのステップが追加されます。 オブジェクトを自分で破棄すると、オブジェクトを収集し、次の GC でメモリを返すことができます。 ファイナライズが必要な場合は、実際のメソッドが呼び出されるまで待つ必要があります。 これがいつ起こるかについての保証は与えられませんので、多くのメモリを縛り付け、最終化キューの慈悲を得ることができます。 これは、オブジェクトがオブジェクトのツリー全体に接続されていて、ファイナライズが行われるまですべてメモリに格納されている場合、非常に問題になる可能性があります。
使用するガベージ コレクターの選択
CLR には、ワークステーション (mscorwks.dll) とサーバー (mscorsvr.dll) の 2 つの異なる GC があります。 ワークステーション モードで実行する場合、待機時間はスペースや効率よりも重要です。 複数のプロセッサとクライアントがネットワーク経由で接続されているサーバーでは、ある程度の待機時間が得られますが、スループットが最優先事項になりました。 これらの両方のシナリオを 1 つの GC スキームにシューホーンするのではなく、Microsoft には、各状況に合わせて調整された 2 つのガベージ コレクターが含まれています。
サーバー GC:
- マルチプロセッサ (MP) スケーラブル、並列
- CPU あたり 1 つの GC スレッド
- マーク中にプログラムが一時停止しました
ワークステーション GC:
- 完全なコレクション中に同時に実行することで、一時停止を最小限に抑えます
サーバー GC は、最大スループットを実現するように設計されており、非常に高いパフォーマンスでスケーリングされます。 サーバーでのメモリの断片化は、ワークステーションよりもはるかに深刻な問題であり、ガベージ コレクションは魅力的な提案です。 単一プロセッサのシナリオでは、両方のコレクターが同じように動作します。ワークステーション モードでは、同時コレクションはありません。 MP マシンでは、Workstation GC は 2 番目のプロセッサを使用してコレクションを同時に実行し、スループットを低下させながら遅延を最小限に抑えます。 サーバー GC では、スループットを最大化し、スケーリングを向上させるために、複数のヒープとコレクション スレッドを使用します。
実行時をホストするときに使用する GC を選択できます。 ランタイムをプロセスに読み込むときは、使用するコレクターを指定します。 API の読み込みについては、「.NET Framework開発者ガイド」を参照してください。 ランタイムをホストし、サーバー GC を選択する単純なプログラムの例については、「付録」を参照してください。
神話:ガベージコレクションは手で行うよりも常に遅い
実際には、コレクションが呼び出されるまで、GC は C で手動で行うよりもはるかに高速です。これは多くの人を驚かせるので、説明する価値があります。 まず、空き領域を見つけることは一定の時間に行われることに注意してください。 すべての空き領域は連続しているため、GC はポインターに従って十分な空き領域があるかどうかを確認します。 C では、 malloc()
を呼び出すと、通常、
無料ブロックのリンクされたリストが検索されます。 これは、特にヒープが不適切に断片化されている場合に、時間がかかる場合があります。 さらに悪いことに、C ランタイムのいくつかの実装では、この手順の間にヒープがロックされます。 メモリが割り当てられるか使用されたら、リストを更新する必要があります。 ガベージ コレクション環境では、割り当ては空きであり、メモリはコレクション中に解放されます。 より高度なプログラマは、大きなメモリ ブロックを予約し、そのブロック自体内の割り当てを処理します。 このアプローチの問題は、メモリの断片化がプログラマにとって大きな問題になり、アプリケーションに多くのメモリ処理ロジックを追加することを強制することです。 最終的に、ガベージ コレクターでは多くのオーバーヘッドは追加されません。 割り当ては同じくらい速く、圧縮は自動的に処理されます。プログラマはアプリケーションに集中できます。
今後、ガベージ コレクターは他の最適化を実行して、さらに高速になる可能性があります。 ホット スポットの識別とキャッシュ使用率の向上が可能であり、速度に大きな違いを生み出す可能性があります。 よりスマートな GC を使用すると、ページをより効率的にパックできるため、実行中に発生するページ フェッチの数を最小限に抑えることができます。 これらのすべてが、ガベージ コレクション環境を手動で行うよりも高速になる可能性があります。
C や C++ などの他の環境で GC を使用できない理由を疑問に思う人もいます。 答えは型です。 これらの言語を使用すると、任意の型へのポインターのキャストが可能になり、ポインターが何を参照しているかを知ることは非常に困難になります。 CLR のようなマネージド環境では、GC を可能にするためにポインターに関して十分な保証を行うことができます。 マネージド ワールドは、GC を実行するためにスレッドの実行を安全に停止できる唯一の場所でもあります。C++ では、これらの操作は安全でないか、非常に制限されています。
速度のチューニング
マネージド ワールドでのプログラムの最大の心配は 、メモリの保持です。 アンマネージド環境で見つかる問題の一部は、マネージド 環境では問題ではありません。メモリ リークとダングル ポインターは、ここではあまり問題になりません。 代わりに、プログラマはリソースが不要になったときに接続したままにすることを注意する必要があります。
パフォーマンスの最も重要なヒューリスティックは、ネイティブ コードの記述に慣れているプログラマにとって最も簡単な方法でもあります。作成する割り当てを追跡し、完了したら解放します。 GC には、周囲に保持されているオブジェクトの一部である場合に作成した 20 KB の文字列を使用しないことを知る方法はありません。 このオブジェクトがどこかのベクターに隠れているのに、その文字列をもう一度使用するつもりはないとします。 フィールドを null に設定すると、他の目的でオブジェクトが必要な場合でも、GC はこれらの 20 KB 後に収集されます。 オブジェクトが不要な場合は、そのオブジェクトへの参照を保持していないことを確認してください。 (ネイティブ コードの場合と同様です)。小さいオブジェクトの場合、これは問題ではありません。 ネイティブ コードのメモリ管理に精通しているプログラマは、ここで問題はありません。同じ一般的なルールがすべて適用されます。 あなたは彼らについてそれほど偏見を持つ必要はありません。
2 つ目の重要なパフォーマンス上の懸念事項は、オブジェクトのクリーンアップに関するものです。 前に説明したように、ファイナライズはパフォーマンスに大きな影響を与えます。 最も一般的な例は、アンマネージド リソースに対するマネージド ハンドラーの例です。何らかのクリーンアップ メソッドを実装する必要があり、ここでパフォーマンスが問題になります。 ファイナライズに依存している場合は、前に一覧表示したパフォーマンスの問題に自分自身を開きます。 覚えておく必要がある他の点は、GC がネイティブの世界のメモリ負荷をほとんど認識しないため、マネージド ヒープ内でポインターを保持するだけで、大量のアンマネージド リソースを使用している可能性があります。 1 つのポインターは大量のメモリを占有しないため、コレクションが必要になるまでしばらく時間がかかる可能性があります。 これらのパフォーマンスの問題を回避するには、メモリの保持に関しては安全に再生しながら、特別なクリーンアップを必要とするすべてのオブジェクトで使用する設計パターンを選択する必要があります。
プログラマは、オブジェクトのクリーンアップを処理するときに 4 つのオプションがあります。
両方を実装する
これは、オブジェクトのクリーンアップに推奨される設計です。 これは、アンマネージド リソースとマネージド リソースを組み合わせたオブジェクトです。 たとえば、 System.Windows.Forms.Control です。 これには、アンマネージド リソース (HWND) と潜在的に管理されるリソース (DataConnection など) があります。 アンマネージド リソースを使用するタイミングがわからない場合は、 で
ILDASM``
プログラムのマニフェストを開き、ネイティブ ライブラリへの参照をチェックできます。 もう 1 つの方法は、 を使用vadump.exe
して、プログラムと共に読み込まれるリソースを確認することです。 どちらの場合も、使用するネイティブ リソースの種類に関する分析情報が得られる場合があります。次のパターンでは、クリーンアップ ロジックをオーバーライドする代わりに、ユーザーに 1 つの推奨される方法を提供します ( Dispose(bool) をオーバーライドします)。 これにより、 Dispose() が呼び出されない場合に備えて、最大限の柔軟性とキャッチオールが提供されます。 最高速度と柔軟性の組み合わせとセーフティネットアプローチにより、これを使用するのに最適な設計になります。
例:
public class MyClass : IDisposable { public void Dispose() { Dispose(true); GC.SuppressFinalizer(this); } protected virtual void Dispose(bool disposing) { if (disposing) { ... } ... } ~MyClass() { Dispose(false); } }
Dispose() のみを実装する
これは、オブジェクトにマネージド リソースのみが含まれており、そのクリーンアップが決定論的であることを確認する場合です。 このようなオブジェクトの例として 、System.Web.UI.Control があります。
例:
public class MyClass : IDisposable { public virtual void Dispose() { ... }
Finalize() のみを実装する
これは非常にまれな状況で必要であり、私はそれに対して強くお勧めします。 Finalize() のみのオブジェクトの影響は、オブジェクトがいつ収集されるのかプログラマには分からず、特別なクリーンアップを必要とするのに十分な複雑なリソースを使用しているということです。 この状況は、適切に設計されたプロジェクトでは発生しません。その中に自分自身が見つかる場合は、戻って何が間違っていたかを調べる必要があります。
例:
public class MyClass { ... ~MyClass() { ... }
どちらも実装しない
これは、破棄も最終処理もされていない他のマネージド オブジェクトのみを指すマネージド オブジェクト用です。
推奨
メモリ管理を処理するための推奨事項は、使い慣れている必要があります。オブジェクトの使用が完了したらオブジェクトを解放し、オブジェクトへのポインターを残すのを注意してください。 オブジェクトのクリーンアップに関しては、アンマネージ リソースを持つオブジェクトに 対して Finalize() メソッドと Dispose()
メソッドの両方を実装します。 これにより、後で予期しない動作が回避され、適切なプログラミングプラクティスが適用されます
ここでの欠点は、ユーザーが Dispose() を呼び出す必要があるということです。 ここではパフォーマンスが低下しませんが、オブジェクトの破棄について考えなければならないことに不満を感じる人もいます。 しかし、私は理にかなっているモデルを使用するのは悪化の価値があると思います。 また、GCが常に面倒を見ることを盲目的に信頼することはできないため、これにより、割り当てるオブジェクトに注意を払う必要があります。 C または C++ のバックグラウンドから来たプログラマの場合、 Dispose() の呼び出しを強制することは、おそらく有益です。
Dispose() は、その下のオブジェクトのツリー内の任意の場所でアンマネージド リソースを保持するオブジェクトでサポートされている必要があります。ただし、 Finalize() は、OS ハンドルやアンマネージ メモリ割り当てなど、これらのリソースに対して特に保持されているオブジェクトにのみ配置する必要があります。 親オブジェクトの Dispose()によって呼び出される Dispose,
() をサポートするだけでなく、Finalize() を実装するための "ラッパー" として小さなマネージド オブジェクトを作成することをお勧めします。 親オブジェクトにはファイナライザーがないため、 Dispose() が呼び出されたかどうかにかかわらず、オブジェクトのツリー全体がコレクションに残りません。
ファイナライザーの経験則として、ファイナライザーは、ファイナライズを 必要とする 最もプリミティブなオブジェクトでのみ使用することをお勧めします。 データベース接続を含む大規模なマネージド リソースがあるとします。接続自体が最終処理されるようにしますが、残りのオブジェクトは破棄可能にします。 このようにして 、Dispose() を呼び出し、接続が終了するのを待たずに、オブジェクトのマネージド部分をすぐに解放できます。 注意: Finalize() は、必要な場合にのみ使用してください。
メモ C および C++ プログラマ: C# のデストラクター セマンティックは、破棄メソッドではなく ファイナライザーを作成します。
スレッド プール
基本操作
CLR のスレッド プールは多くの点で NT スレッド プールに似ていますが、プログラマ側ではほとんど新しい理解を必要としません。 これには待機スレッドがあり、他のスレッドのブロックを処理し、戻る必要があるときに通知し、他の作業を行うための解放を行うことができます。 新しいスレッドを生成し、実行時に CPU 使用率を最適化するために他のスレッドをブロックして、最大限の有用な作業が行われることを保証できます。 また、スレッドが完了するとスレッドがリサイクルされ、新しいスレッドを殺して生成するオーバーヘッドなしで再び起動されます。 これは、スレッドを手動で処理する場合よりもパフォーマンスが大幅に向上しますが、キャッチオールではありません。 スレッド 化されたアプリケーションをチューニングする場合は、スレッド プールを使用するタイミングを把握することが不可欠です。
NT スレッド プールから知っていること:
- スレッド プールは、スレッドの作成とクリーンアップを処理します。
- I/O スレッドの完了ポートが提供されます (NT プラットフォームのみ)。
- コールバックは、ファイルまたはその他のシステム リソースにバインドできます。
- タイマー API と待機 API を使用できます。
- スレッド プールは、最後の挿入からの遅延、現在のスレッドの数、キューのサイズなどのヒューリスティックを使用して、アクティブにする必要があるスレッドの数を決定します。
- スレッドは、共有キューからフィードします。
.NET の違い:
- マネージド コードでブロックされているスレッド (ガベージ コレクション、マネージド待機など) を認識し、それに応じてスレッド 挿入ロジックを調整できます。
- 個々のスレッドに対するサービスの保証はありません。
スレッドを自分で処理するタイミング
スレッド プールを効果的に使用することは、スレッドから必要なものを知ることと密接に結び付けられます。 サービスの保証が必要な場合は、自分で管理する必要があります。 ほとんどの場合、プールを使用すると最適なパフォーマンスが得られます。 厳しい制限があり、スレッドを厳密に制御する必要がある場合は、ネイティブ スレッドを使用する方が理にかなっている可能性があるため、マネージド スレッドを自分で処理することを注意してください。 マネージド コードを記述し、スレッド処理を独自に処理する場合は、接続ごとにスレッドを生成しないようにしてください。これはパフォーマンスを低下させるだけです。 経験則として、まれに実行される大規模で時間のかかるタスクがある非常に具体的なシナリオでは、マネージド ワールドでスレッドを自分で処理することを選択する必要があります。 たとえば、大きなキャッシュをバックグラウンドで埋めたり、大きなファイルをディスクに書き込んだりします。
速度のチューニング
スレッド プールは、アクティブにするスレッドの数に制限を設定し、それらのスレッドの多くがブロックすると、プールは枯渇します。 理想的には、有効期間の短い非ブロッキング スレッドにスレッド プールを使用する必要があります。 サーバー アプリケーションでは、各要求に迅速かつ効率的に応答する必要があります。 要求ごとに新しいスレッドを起動すると、多くのオーバーヘッドが発生します。 解決策は、スレッドをリサイクルし、完了時にすべてのスレッドの状態をクリーンして返すように注意することです。 これらは、スレッド プールが主要なパフォーマンスと設計上の勝利であり、テクノロジを十分に活用する必要があるシナリオです。 スレッド プールは状態のクリーンアップを処理し、特定の時点で最適な数のスレッドが使用されていることを確認します。 他の状況では、スレッド処理を自分で処理する方が理にかなっている場合があります。
CLR では、型セーフを使用して、AppDomain が同じプロセスを確実に共有できるようにプロセスに関する保証を行うことができますが、そのような保証はスレッドと共に存在しません。 プログラマは、適切に動作するスレッドを記述する責任があり、ネイティブ コードからのすべての知識が引き続き適用されます。
次に、スレッド プールを利用する単純なアプリケーションの例を示します。 多数のワーカー スレッドを作成し、閉じる前に単純なタスクを実行させます。 私はいくつかのエラーチェックを取りましたが、これはフレームワークSDKフォルダの「Samples\Threading\Threadpool」にあるのと同じコードです。 この例では、単純な作業項目を作成し、threadpool を使用して複数のスレッドでこれらの項目を処理するコードを用意しています。プログラマがこれらの項目を管理する必要はありません。 詳細については、ReadMe.html ファイルを参照してください。
using System;
using System.Threading;
public class SomeState{
public int Cookie;
public SomeState(int iCookie){
Cookie = iCookie;
}
};
public class Alpha{
public int [] HashCount;
public ManualResetEvent eventX;
public static int iCount = 0;
public static int iMaxCount = 0;
public Alpha(int MaxCount) {
HashCount = new int[30];
iMaxCount = MaxCount;
}
// The method that will be called when the Work Item is serviced
// on the Thread Pool
public void Beta(Object state){
Console.WriteLine(" {0} {1} :",
Thread.CurrentThread.GetHashCode(), ((SomeState)state).Cookie);
Interlocked.Increment(ref HashCount[Thread.CurrentThread.GetHashCode()]);
// Do some busy work
int iX = 10000;
while (iX > 0){ iX--;}
if (Interlocked.Increment(ref iCount) == iMaxCount) {
Console.WriteLine("Setting EventX ");
eventX.Set();
}
}
};
public class SimplePool{
public static int Main(String[] args) {
Console.WriteLine("Thread Simple Thread Pool Sample");
int MaxCount = 1000;
ManualResetEvent eventX = new ManualResetEvent(false);
Console.WriteLine("Queuing {0} items to Thread Pool", MaxCount);
Alpha oAlpha = new Alpha(MaxCount);
oAlpha.eventX = eventX;
Console.WriteLine("Queue to Thread Pool 0");
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),new SomeState(0));
for (int iItem=1;iItem < MaxCount;iItem++){
Console.WriteLine("Queue to Thread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(new WaitCallback(oAlpha.Beta),
new SomeState(iItem));
}
Console.WriteLine("Waiting for Thread Pool to drain");
eventX.WaitOne(Timeout.Infinite,true);
Console.WriteLine("Thread Pool has been drained (Event fired)");
Console.WriteLine("Load across threads");
for(int iIndex=0;iIndex<oAlpha.HashCount.Length;iIndex++)
Console.WriteLine("{0} {1}", iIndex, oAlpha.HashCount[iIndex]);
}
return 0;
}
}
The JIT
基本操作
他の VM と同様に、CLR には中間言語をネイティブ コードにコンパイルする方法が必要です。 CLR で実行するプログラムをコンパイルすると、コンパイラはソースを高レベル言語から MSIL (Microsoft Intermediate Language) とメタデータの組み合わせに変換します。 これらは PE ファイルにマージされ、任意の CLR 対応マシンで実行できます。 この実行可能ファイルを実行すると、JIT は IL のネイティブ コードへのコンパイルを開始し、そのコードを実際のマシンで実行します。 これはメソッドごとに行われるので、JITing の遅延は、実行するコードに必要な期間だけです。
JIT は非常に高速であり、非常に優れたコードを生成します。 実行される最適化の一部 (およびそれぞれの説明) については、以下で説明します。 これらの最適化のほとんどには、JIT に多くの時間を費やさないことを確認するための制限があることに注意してください。
定数フォールディング - コンパイル時に定数値を計算します。
変更前 クリック後 x = 5 + 7
x = 12
定数伝達とコピー伝達 - 以前のフリー変数に後方に置き換えます。
変更前 クリック後 x = a
x = a
y = x
y = a
z = 3 + y
z = 3 + a
メソッドインライン化 - args を呼び出し時に渡された値に置き換え、呼び出しを排除します。 その後、他の多くの最適化を実行して、デッド コードを切り取ることができます。 速度上の理由から、現在の JIT にはインラインで実行できる境界がいくつかあります。 たとえば、インライン化されるのは小さなメソッド (IL サイズが 32 未満) のみで、フロー制御分析はかなりプリミティブです。
変更前 クリック後 ...
x=foo(4, true);
...
}
foo(int a, bool b){
if(b){
return a + 5;
} else {
return 2a + bar();
}
...
x = 9
...
}
foo(int a, bool b){
if(b){
return a + 5;
} else {
return 2a + bar();
}
[コード ホイストとドミネーター] - ループが外部で重複している場合は、内部ループからコードを削除します。 次の 'before' の例は、すべての配列インデックスをチェックする必要があるため、実際には IL レベルで生成されます。
変更前 クリック後 for(i=0; i< a.length;i++){
if(i < a.length()){
a[i] = null
} else {
raise IndexOutOfBounds;
}
}
for(int i=0; i<a.length; i++){
a[i] = null;
}
ループの登録解除 — カウンターをインクリメントしてテストを実行するオーバーヘッドを取り除き、ループのコードを繰り返すことができます。 非常にタイトなループの場合、パフォーマンスが向上します。
変更前 クリック後 for(i=0; i< 3; i++){
print("flaming monkeys!");
}
print("flaming monkeys!");
print("flaming monkeys!");
print("flaming monkeys!");
一般的な SubExpression の削除 — 再計算される情報がライブ変数にまだ含まれている場合は、代わりに使用します。
変更前 クリック後 x = 4 + y
z = 4 + y
x = 4 + y
z = x
登録 — ここでコード例を示すのは役に立たないため、説明で十分です。 この最適化では、ローカルと temp が関数でどのように使用されているかを調べることで時間を費やし、レジスタの割り当てを可能な限り効率的に処理しようとします。 これは非常にコストの高い最適化になる可能性があり、現在の CLR JIT では、登録のために最大 64 個のローカル変数のみが考慮されます。 考慮されない変数は、スタック フレームに配置されます。 これはJITingの制限の典型的な例です。これは時間の99%で問題ありませんが、100以上のローカルを持つ非常に珍しい関数は、従来の時間のかかる事前コンパイルを使用して最適化されます。
その他— その他の単純な最適化が実行されますが、上記の一覧は適切なサンプルです。 JIT では、デッドコードやその他のピープホール最適化のパスも実行されます。
コードが JITED になるのはいつですか?
コードの実行時にコードが通過するパスを次に示します。
- プログラムが読み込まれ、関数テーブルが IL を参照するポインターで初期化されます。
- Main メソッドは、ネイティブ コードに JITed され、実行されます。 関数の呼び出しは、テーブルを介して間接関数呼び出しにコンパイルされます。
- 別のメソッドが呼び出されると、実行時にテーブルが JITed コードを指しているかどうかを確認します。
- が存在する場合 (おそらく、別の呼び出しサイトから呼び出されたか、プリコンパイル済みである場合)、制御フローは続行されます。
- そうでない場合、メソッドは JITed であり、テーブルが更新されます。
- 呼び出されると、ネイティブ コードにコンパイルされるメソッドが増え、テーブル内のエントリが増え続ける x86 命令のプールを指します。
- プログラムが実行されると、JIT は、すべてがコンパイルされるまで呼び出される頻度が低くなります。
- メソッドは、呼び出されるまで JITed ではなく、プログラムの実行中にもう一度 JITed されることはありません。 使用した分だけ支払います。
神話: JITED プログラムの実行速度がプリコンパイル済みプログラムよりも遅い
このようなケースはまれです。 いくつかのメソッドの JITing に関連するオーバーヘッドは、ディスクからの数ページの読み取りに費やされた時間と比較して軽微であり、メソッドは必要なときにのみ JITed されます。 JIT で費やされる時間は非常に短いので、ほとんど目立ちません。メソッドが JITed されると、そのメソッドのコストが再び発生することはありません。 これについては、「プリコンパイル コード」セクションで詳しく説明します。
前述のように、バージョン 1 (v1) JIT はコンパイラが行う最適化の大部分を実行し、より高度な最適化が追加されるため、次のバージョン (vNext) でのみ高速になります。 さらに重要なのは、JIT では、CPU 固有の最適化やキャッシュ チューニングなど、通常のコンパイラでは実行できない最適化をいくつか実行できることです。
JIT-Onlyの最適化
JIT は実行時にアクティブ化されるため、コンパイラが認識していない情報が多数あります。 これにより、実行時にのみ使用できるいくつかの最適化を実行できます。
- プロセッサ固有の最適化 - 実行時に、JIT は SSE 命令または 3DNow 命令を使用できるかどうかを認識します。 実行可能ファイルは、P4、Athlon、または将来のプロセッサ ファミリ用に特別にコンパイルされます。 1 回デプロイすると、JIT とユーザーのコンピューターと共に同じコードが改善されます。
- 関数とオブジェクトの場所は実行時に使用できるため、間接参照のレベルを最適化します。
- JIT はアセンブリ間で最適化を実行でき、静的ライブラリを使用してプログラムをコンパイルするときに得られる利点の多くを提供しますが、動的ライブラリを使用する柔軟性と小さなフットプリントを維持できます。
- 実行時に制御フローを認識するため、より頻繁に呼び出される関数を積極的にインライン化します。 最適化によって大幅な速度ブーストが提供され、vNext で追加の改善の余地が大きくなります。
これらの実行時の改善は、小規模な 1 回限りのスタートアップ コストを犠牲にして行われ、JIT で費やされた時間を相殺する以上の可能性があります。
コードのプリコンパイル (ngen.exeを使用)
アプリケーション ベンダーにとって、インストール時にコードをプリコンパイルする機能は魅力的なオプションです。 Microsoft では、 形式 ngen.exe
でこのオプションを提供します。これにより、プログラム全体で通常の JIT コンパイラを 1 回実行し、結果を保存できます。 プリコンパイル中は実行時のみの最適化を実行できないため、生成されるコードは通常、通常の JIT で生成されるコードほど適切ではありません。 ただし、JIT メソッドをその場で実行しなくても、起動コストが大幅に削減され、一部のプログラムの起動速度が著しく向上します。 将来、ngen.exeは、単に同じランタイム JIT を実行するよりも多くのことを行う可能性があります。実行時よりも高い境界を持つより積極的な最適化、開発者への読み込み順序最適化の公開 (VM ページへのコードのパック方法の最適化)、プリコンパイル中の時間を利用できるより複雑で時間のかかる最適化です。
スタートアップ時間を短縮することは 2 つのケースで役立ちます。それ以外の場合は、通常の JITing で実行できる実行時のみの最適化とは競合しません。 最初の状況では、プログラムの早い段階で膨大な数のメソッドを呼び出します。 多くの方法を前もって JIT する必要があり、その結果、許容できない読み込み時間が発生します。 これはほとんどの人には当てはまりませんが、PREJITingがあなたに影響を与える場合は理にかなっているかもしれません。 プリコンパイルは、大きな共有ライブラリの場合にも理にかなっています。これは、読み込みのコストがはるかに多いためです。 ほとんどのアプリケーションではそれらを使用するため、Microsoft は CLR のフレームワークをプリコンパイルします。
プリコンパイルが答えであるかどうかを確認するためにngen.exe
を使用するのは簡単なので、試してみることをお勧めします。ただし、ほとんどの場合、通常の JIT を使用し、実行時の最適化を利用することをお勧めします。 彼らは大きなペイオフを持っており、ほとんどの状況で1回限りのスタートアップコストを相殺する以上のものになります。
速度のチューニング
プログラマにとって注目に値するものは 2 つだけです。 まず、JIT は非常にインテリジェントです。 コンパイラを考え抜こうとしないでください。 通常どおりにコーディングします。 たとえば、次のコードがあるとします。
...
|
...
|
一部のプログラマは、右側の例のように、長さの計算を移動して一時に保存することで、速度を向上できると考えています。
実際、このような最適化は 10 年近く役に立ちません。最新のコンパイラは、この最適化を実行できる以上の機能です。 実際、このようなことが実際にパフォーマンスを損なうことがあります。 上記の例では、コンパイラは myArray の長さが定数であることを確認し、for ループの比較に定数を挿入チェック可能性があります。 しかし、右側のコードは、ループ全体を通してライブであるため l
、この値をレジスタに格納する必要があるとコンパイラをだます可能性があります。 要するに、最も読みやすく、最も理にかなっているコードを記述します。 コンパイラを考え抜こうとすることは役に立たず、時には痛くなる可能性があります。
2 つ目は、テールコールです。 現時点では、C# コンパイラと Microsoft® Visual Basic® コンパイラでは、末尾呼び出しを使用するように指定する機能は提供されていません。 この機能が本当に必要な場合、1 つのオプションは、逆アセンブラーで PE ファイルを開き、代わりに MSIL .tail 命令を使用することです。 これはエレガントなソリューションではありませんが、末尾呼び出しは、Scheme や ML などの言語であるため、C# と Visual Basic では役に立ちません。 末尾呼び出しを実際に利用する言語のコンパイラを記述するPeopleは、必ずこの命令を使用する必要があります。 ほとんどの人にとっての現実は、テールコールを使用するようにILを手動で調整しても、大きな速度の利点は得られないということです。 セキュリティ上の理由から、実行時に実際にこれらを通常の呼び出しに戻す場合があります。 おそらく将来のバージョンでは、テールコールをサポートするためにより多くの労力が必要になりますが、現時点ではパフォーマンスの向上がそれを保証するには不十分であり、それを利用したいプログラマはほとんどありません。
AppDomain
基本操作
プロセス間通信はますます一般的になっています。 安定性とセキュリティ上の理由から、OS はアプリケーションを別々のアドレス空間に保持します。 簡単な例は、すべての 16 ビット アプリケーションを NT で実行する方法です。別のプロセスで実行すると、あるアプリケーションが別のアプリケーションの実行に干渉することはできません。 ここでの問題は、コンテキスト スイッチのコストと、プロセス間の接続を開くコストです。 この操作は非常にコストがかかり、パフォーマンスが大幅に低下します。 多くの場合、複数の Web アプリケーションをホストするサーバー アプリケーションでは、これはパフォーマンスとスケーラビリティの両方を大幅に低下させます。
CLR では、AppDomain の概念が導入されています。これは、アプリケーションの自己完結型スペースであるという点でプロセスに似ています。 ただし、AppDomains はプロセスごとに 1 つに制限されません。 マネージド コードによって提供されるタイプ セーフにより、まったく関係のない 2 つの AppDomain を同じプロセスで実行できます。 通常、プロセス間通信のオーバーヘッドに多くの実行時間を費やす状況では、パフォーマンスの向上は非常に大きくなります。アセンブリ間の IPC は、NT のプロセス間よりも 5 倍高速です。 このコストを大幅に削減することで、プログラム設計時に速度ブーストと新しいオプションの両方が得られます。以前はコストが高すぎる可能性がある別のプロセスを使用するのが理にかなっています。 以前と同じセキュリティで同じプロセスで複数のプログラムを実行できることは、スケーラビリティとセキュリティに大きな影響を与える。
AppDomains のサポートは OS に存在しません。 AppDomain は、ASP.NET、シェル実行可能ファイル、Microsoft インターネット エクスプローラーに存在するものなど、CLR ホストによって処理されます。 独自に記述することもできます。 各ホストは 、既定のドメインを指定します。これは、アプリケーションが最初に起動したときに読み込まれ、プロセスが終了したときにのみ閉じられます。 プロセスに他のアセンブリを読み込む場合は、特定の AppDomain に読み込まれるように指定し、それぞれに異なるセキュリティ ポリシーを設定できます。 詳細については、Microsoft .NET Framework SDK のドキュメントを参照してください。
速度のチューニング
AppDomains を効果的に使用するには、作成しているアプリケーションの種類と、どのような作業を行う必要があるかを考える必要があります。 AppDomains は、アプリケーションが次の特性の一部に適合する場合に最も効果的です。
- 多くの場合、それ自体の新しいコピーが生成されます。
- 他のアプリケーションと連携して情報 (Web サーバー内のデータベース クエリなど) を処理します。
- IPC では、アプリケーションと排他的に動作するプログラムに多くの時間を費やします。
- 他のプログラムを開いて閉じます。
AppDomains が役立つ状況の例は、複雑な ASP.NET アプリケーションで確認できます。 異なる vRoot 間で分離を強制する必要があるとします。ネイティブ空間では、各 vRoot を個別のプロセスに配置する必要があります。 これは非常にコストがかかり、コンテキスト間の切り替えは多くのオーバーヘッドです。 マネージド ワールドでは、各 vRoot を個別の AppDomain にすることができます。 これにより、オーバーヘッドを大幅に削減しながら、必要な分離が維持されます。
AppDomains は、アプリケーションが複雑で、他のプロセスやそれ自体の他のインスタンスと密接に連携する必要がある場合にのみ使用する必要があります。 iter-AppDomain 通信はプロセス間通信よりもはるかに高速ですが、AppDomain の開始と終了のコストは実際には高くなる可能性があります。 AppDomains は、間違った理由で使用するとパフォーマンスが低下する可能性があるため、適切な状況で使用していることを確認してください。 マネージド コードのみを AppDomain に読み込むことができることに注意してください。アンマネージド コードはセキュリティで保護されるとは限りません。
複数の AppDomain 間で共有されるアセンブリは、ドメイン間の分離を維持するために、すべてのドメインに対して JITed である必要があります。 これにより、多くの重複するコードが作成され、メモリが無駄になります。 何らかの XML サービスを使用して要求に応答するアプリケーションの場合を考えてみましょう。 特定の要求を相互に分離しておく必要がある場合は、それらを異なる AppDomain にルーティングする必要があります。 ここでの問題は、すべての AppDomain で同じ XML ライブラリが必要になり、同じアセンブリが複数回読み込まれるということです。
これを回避する 1 つの方法は、アセンブリを Domain-Neutral として宣言することです。つまり、直接参照が許可されておらず、間接参照によって分離が適用されます。 これにより、アセンブリが 1 回だけ JITed されるため、時間を節約できます。 また、何も複製されないので、メモリも節約されます。 残念ながら、間接参照が必要なため、パフォーマンスに影響します。 アセンブリをドメインに依存しないアセンブリとして宣言すると、メモリが懸念される場合や、時間がかかりすぎる場合に JITing コードが無駄になると、パフォーマンスが向上します。 このようなシナリオは、複数のドメインで共有される大規模なアセンブリの場合に一般的です。
セキュリティ
基本操作
コード アクセス セキュリティは、強力で非常に便利な機能です。 これにより、ユーザーは半信頼コードを安全に実行でき、悪意のあるソフトウェアやいくつかの種類の攻撃から保護され、リソースへの制御された ID ベースのアクセスが可能になります。 ネイティブ コードでは、型の安全性がほとんどなく、プログラマがメモリを処理するため、セキュリティを提供することは非常に困難です。 CLR では、実行時にコードの実行について十分に認識され、強力なセキュリティ サポートが追加されます。これは、ほとんどのプログラマにとって新しい機能です。
セキュリティは、アプリケーションの速度とワーキング セットサイズの両方に影響します。 また、プログラミングのほとんどの領域と同様に、開発者がセキュリティをどのように使用するかによって、パフォーマンスへの影響を大きく判断できます。 セキュリティ システムはパフォーマンスを念頭に置いて設計されており、ほとんどの場合、アプリケーション開発者が考えも考えもせず、適切に実行する必要があります。 ただし、セキュリティ システムからパフォーマンスの最後のビットを絞り込むには、いくつかの方法があります。
速度のチューニング
通常、セキュリティ チェックを実行するには、現在のメソッドを呼び出すコードに正しいアクセス許可があることを確認するためのスタック ウォークが必要です。 ランタイムには、スタック全体の歩き方を避けるのに役立つ最適化がいくつかありますが、プログラマが役立つ操作がいくつかあります。 これにより、宣言セキュリティは型またはそのメンバーをさまざまなアクセス許可で装飾しますが、命令型セキュリティはセキュリティ オブジェクトを作成し、それに対する操作を実行します。
- 宣言型セキュリティは、 Assert、 Deny 、 PermitOnly に最も速くアクセスする方法です。 通常、これらの操作では、正しい呼び出しフレームを見つけるためにスタック ウォークが必要ですが、これらの修飾子を明示的に宣言すると回避できます。 命令的に実行すると、要求が高速になります。
- アンマネージ コードとの相互運用を行うときは、SuppressUnmanagedCodeSecurity 属性を使用して実行時のセキュリティ チェックを削除できます。 これにより、チェックがリンク時間に移動します。これははるかに高速です。 注意として、コードが他のコードにセキュリティ ホールを公開していないことを確認してください。これにより、削除されたチェックが安全でないコードに悪用される可能性があります。
- ID チェックは、コード チェックよりもコストが高くなります。 LinkDemand を使用して、代わりにリンク時にこれらのチェックを行うことができます。
セキュリティを最適化するには、次の 2 つの方法があります。
- 実行時ではなく、リンク時にチェックを実行します。
- セキュリティ チェックは、命令型ではなく宣言型にします。
最初に集中する必要があるのは、これらのチェックをできるだけ多く移動して、時間をリンクすることです。 これはアプリケーションのセキュリティに影響を与える可能性があるので、実行時の状態に依存するリンカーにチェックを移動しないように注意してください。 リンク時間にできるだけ移動したら、宣言型または命令型のセキュリティを使用して実行時チェックを最適化する必要があります。使用する特定の種類のチェックに最適なを選択します。
リモート処理
基本操作
.NET のリモート処理テクノロジは、豊富な種類のシステムと CLR の機能をネットワーク経由で拡張します。 XML、SOAP、HTTP を使用すると、プロシージャを呼び出し、同じコンピューターでホストされているかのように、リモートでオブジェクトを渡すことができます。 これは、機能のスーパーセットを提供するという点で、DCOM または CORBA の .NET バージョンと考えることができます。
これは、複数のサーバーが異なるサービスをホストしている場合に、それらのサービスをシームレスにリンクするために相互に通信するサーバー環境で特に便利です。 機能を失うことなく複数のコンピューター間でプロセスを物理的に分割できるため、スケーラビリティも向上します。
速度のチューニング
リモート処理では多くの場合、ネットワーク待機時間の観点からペナルティが発生するため、CLR には常に同じ規則が適用されます。送信するトラフィックの量を最小限に抑え、プログラムの残りの部分でリモート呼び出しが返されるのを待機しないようにします。 リモート処理を使用してパフォーマンスを最大化する場合に使用する適切なルールをいくつか次に示します。
- Chatty 通話の代わりにチャンキーにする - リモートで行う必要がある通話の数を減らすことができるかどうかを確認します。 たとえば、 get() メソッドと set() メソッドを使用して、リモート オブジェクトのプロパティをいくつか設定するとします。 オブジェクトをリモートで再作成するだけで、作成時にこれらのプロパティが設定される時間を節約できます。 これは 1 つのリモート呼び出しを使用して行うことができるため、ネットワーク トラフィックの無駄な時間を節約できます。 場合によっては、オブジェクトをローカル コンピューターに移動し、そこでプロパティを設定してからコピーし直すのが理にかなっている場合があります。 帯域幅と待機時間によっては、1 つのソリューションが他のソリューションよりも理にかなっている場合があります。
- CPU 負荷とネットワーク負荷のバランスを取る - ネットワーク経由で何かを送信することが理にかなっている場合もあれば、自分で作業を行う方が良い場合もあります。 ネットワークを横断する時間を大幅に無駄にした場合、パフォーマンスが低下します。 CPU を使い切りすぎると、他の要求に応答できなくなります。 これらの 2 つの間で適切なバランスを見つけることは、アプリケーションをスケーリングするために不可欠です。
- 非同期呼び出しを使用する - ネットワーク経由で呼び出しを行う場合は、特に必要でない限り非同期であることを確認します。 それ以外の場合、アプリケーションは応答を受け取るまで停止し、ユーザー インターフェイスまたは大量のサーバーでは受け入れられない可能性があります。 見る良い例は、.NET に付属する Framework SDK の 「Samples\technologies\remoting\advanced\asyncdelegate」にあります。
- [オブジェクトを最適に使用] - すべての要求に対して新しいオブジェクトを作成するか (SingleCall) するか、同じオブジェクトをすべての要求に使用することを指定できます (シングルトン)。 すべての要求に対して 1 つのオブジェクトを持つことは、リソースを集中的に消費することは確かに少なくなりますが、要求から要求までのオブジェクトの同期と構成に注意する必要があります。
- プラグ可能なチャネルとフォーマッタを使用する - リモート処理の強力な機能は、任意のチャネルまたはフォーマッタをアプリケーションに接続できることです。 たとえば、ファイアウォールを通過する必要がない限り、HTTP チャネルを使用する理由はありません。 TCP チャネルを接続すると、パフォーマンスが大幅に向上します。 最適なチャネルまたはフォーマッタを選択してください。
ValueTypes
基本操作
オブジェクトによって与えられる柔軟性は小さい性能価格で来る。 ヒープマネージド オブジェクトは、スタックで管理されるオブジェクトよりも、割り当て、アクセス、および更新に時間がかかります。 このため、たとえば、C++ の構造体は オブジェクトよりもはるかに効率的です。 もちろん、オブジェクトは構造体ではできないことを実行でき、はるかに汎用性が高くなります。
ただし、その柔軟性が必要ない場合もあります。 構造体のような単純なものが 必要 で、パフォーマンス コストを支払いたくない場合があります。 CLR には ValueType と呼ばれるものを指定する機能が用意されており、コンパイル時には構造体と同様に扱われます。 ValueTypes はスタックによって管理され、構造体のすべての速度を提供します。 予想通り、構造体の柔軟性も制限されています (たとえば、継承はありません)。 しかし、必要なのが構造体の場合、ValueTypes は驚くべきスピードブーストを提供します。 ValueTypes とその他の CLR 型システムの詳細については、MSDN ライブラリを参照してください。
速度のチューニング
ValueTypes は、構造体として使用する場合にのみ役立ちます。 ValueType をオブジェクトとして扱う必要がある場合は、実行時にオブジェクトのボックス化とボックス化解除が処理されます。 しかし、これは最初にオブジェクトとして作成するよりもさらに高価です!
多数のオブジェクトと ValueTypes の作成にかかる時間を比較する単純なテストの例を次に示します。
using System;
using System.Collections;
namespace ConsoleApplication{
public struct foo{
public foo(double arg){ this.y = arg; }
public double y;
}
public class bar{
public bar(double arg){ this.y = arg; }
public double y;
}
class Class1{
static void Main(string[] args){
Console.WriteLine("starting struct loop....");
int t1 = Environment.TickCount;
for (int i = 0; i < 25000000; i++) {
foo test1 = new foo(3.14);
foo test2 = new foo(3.15);
if (test1.y == test2.y) break; // prevent code from being
eliminated JIT
}
int t2 = Environment.TickCount;
Console.WriteLine("struct loop: (" + (t2-t1) + "). starting object
loop....");
t1 = Environment.TickCount;
for (int i = 0; i < 25000000; i++) {
bar test1 = new bar(3.14);
bar test2 = new bar(3.15);
if (test1.y == test2.y) break; // prevent code from being
eliminated JIT
}
t2 = Environment.TickCount;
Console.WriteLine("object loop: (" + (t2-t1) + ")");
}
ご自身で試してみてください。 時間差は数秒の順序です。 次に、実行時に構造体をボックス化してボックス化解除する必要が生じるようにプログラムを変更してみましょう。 ValueType を使用する速度の利点が完全に消えました。 ここでの道徳的な点は、ValueTypes はオブジェクトとして使用しない非常にまれな状況でのみ使用されるということです。 パフォーマンスの勝利は、適切に使用すると非常に大きくなることが多いため、このような状況に注意することが重要です。
using System;
using System.Collections;
namespace ConsoleApplication{
public struct foo{
public foo(double arg){ this.y = arg; }
public double y;
}
public class bar{
public bar(double arg){ this.y = arg; }
public double y;
}
class Class1{
static void Main(string[] args){
Hashtable boxed_table = new Hashtable(2);
Hashtable object_table = new Hashtable(2);
System.Console.WriteLine("starting struct loop...");
for(int i = 0; i < 10000000; i++){
boxed_table.Add(1, new foo(3.14));
boxed_table.Add(2, new foo(3.15));
boxed_table.Remove(1);
}
System.Console.WriteLine("struct loop complete.
starting object loop...");
for(int i = 0; i < 10000000; i++){
object_table.Add(1, new bar(3.14));
object_table.Add(2, new bar(3.15));
object_table.Remove(1);
}
System.Console.WriteLine("All done");
}
}
}
Microsoft では、ValueTypes を大きな方法で使用しています。フレームワーク内のすべてのプリミティブは ValueTypes です。 私の推奨事項は、構造体に対してかゆみを感じるたびに ValueTypes を使用することです。 ボックス化/ボックス化解除を行わない限り、彼らは巨大なスピードブーストを提供することができます。
重要な点の 1 つは、相互運用シナリオで ValueTypes にマーシャリングが必要ない点です。 マーシャリングはネイティブ コードとの相互運用時に最大のパフォーマンス ヒットの 1 つであるため、ネイティブ関数の引数として ValueTypes を使用することは、おそらく実行できる最大のパフォーマンス調整の 1 つです。
その他のリソース
.NET Frameworkのパフォーマンスに関する関連トピックは次のとおりです。
設計、アーキテクチャ、コーディングの哲学の概要、マネージド世界でのパフォーマンス分析ツールのチュートリアル、現在利用可能な他のエンタープライズ アプリケーションとの .NET のパフォーマンス比較など、現在開発中の今後の記事をご覧ください。
付録: サーバーのランタイムのホスティング
#include "mscoree.h"
#include "stdio.h"
#import "mscorlib.tlb" named_guids no_namespace raw_interfaces_only \
no_implementation exclude("IID_IObjectHandle", "IObjectHandle")
long main(){
long retval = 0;
LPWSTR pszFlavor = L"svr";
// Bind to the Run time.
ICorRuntimeHost *pHost = NULL;
HRESULT hr = CorBindToRuntimeEx(NULL,
pszFlavor,
NULL,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void **)&pHost);
if (SUCCEEDED(hr)){
printf("Got ICorRuntimeHost\n");
// Start the Run time (this also creates a default AppDomain)
hr = pHost->Start();
if(SUCCEEDED(hr)){
printf("Started\n");
// Get the Default AppDomain created when we called Start
IUnknown *pUnk = NULL;
hr = pHost->GetDefaultDomain(&pUnk);
if(SUCCEEDED(hr)){
printf("Got IUnknown\n");
// Ask for the _AppDomain Interface
_AppDomain *pDomain = NULL;
hr = pUnk->QueryInterface(IID__AppDomain, (void**)&pDomain);
if(SUCCEEDED(hr)){
printf("Got _AppDomain\n");
// Execute Assembly's entry point on this thread
BSTR pszAssemblyName = SysAllocString(L"Managed.exe");
hr = pDomain->ExecuteAssembly_2(pszAssemblyName, &retval);
SysFreeString(pszAssemblyName);
if (SUCCEEDED(hr)){
printf("Execution completed\n");
//Execution completed Successfully
pDomain->Release();
pUnk->Release();
pHost->Stop();
return retval;
}
}
pDomain->Release();
pUnk->Release();
}
}
pHost->Release();
}
printf("Failure, HRESULT: %x\n", hr);
// If we got here, there was an error, return the HRESULT
return hr;
}
この記事に関する質問やコメントがある場合は、プログラム マネージャーの Claudio Caldato に.NET Frameworkパフォーマンスの問題についてお問い合わせください。