印刷用ページ       送信     
クリックして評価とフィードバックをお寄せください
MSDN
MSDN ライブラリ
テクニカルドキュメント
.NET 開発
patterns & practices
 .NET アプリケーションのパフォーマンスとスケーラビリティの向上 -...

  低帯域幅での表示をオンにする
第 5 章 「マネージ コード パフォーマンスの向上」

Patterns and Practices ホーム

.NET アプリケーションのパフォーマンスとスケーラビリティの向上

J.D. Meier, Srinath Vasireddy, Ashish Babbar, and Alex Mackman
Microsoft Corporation

May 2004
日本語版最終更新日 2005 年 8 月 17 日

要約: 高いパフォーマンスを求めるアプリケーションに適した共通言語ランタイム (CLR) を作るのに、多大な労力が費やされました。しかし、CLR の機能を生かすか無駄にするかは、マネージ コードの書き方次第です。この章では、最適なマネージ コードを作るために知っておくべき、パフォーマンスに関連する主要問題を特定します。また、よく見られるミスや、マネージ コードのパフォーマンスを向上させる数々の方法を紹介していきます。

目次

目標
概要
この章の使いかた
アーキテクチャ
パフォーマンスとスケーラビリティに関する問題
設計上の考慮事項
クラス設計に関する考慮事項
実装に関する考慮事項
ガベージ コレクションについての説明
ガベージ コレクションに関するガイドライン
Finalize と Dispose についての説明
Dispose パターン
Finalize と Dispose に関するガイドライン
ピニング(固定化)
スレッド処理についての説明
スレッド処理に関するガイドライン
非同期呼び出しについての説明
非同期に関するガイドライン
ロックと同期についての説明
ロックと同期に関するガイドライン
値型と参照型
ボックス化とボックス化解除についての説明
ボックス化とボックス化解除に関するガイドライン
例外管理
反復処理とループ処理
文字列処理
配列
コレクションについての説明
コレクションに関するガイドライン
コレクション型
リフレクションと遅延バインディング
コード アクセス セキュリティ
ワーキング セットに関する考慮事項
Ngen.exe についての説明
Ngen.exe に関するガイドライン
まとめ
補足資料

目標

  • アセンブリとクラスの設計を最適化する。
  • ガベージ コレクション (GC) の効率を最大化する。
  • Finalize と Dispose を適切に使う。
  • ボックス化オーバーヘッドを最小化する。
  • リフレクションと遅延バインディングの使用について評価する。
  • 例外処理コードを最適化する。
  • 反復構造とループ構造を効率的に使う。
  • 文字列連結を最適化する。
  • 最適なコレクション型を判断し選択する。
  • よくあるスレッド処理ミスを避ける。
  • 非同期呼び出しを効果的かつ効率的に行う。
  • 効率的なロック方法と同期方法を構築する。
  • アプリケーションのワーキング セットを減らす。
  • パフォーマンスに関する考慮事項をコード アクセス セキュリティに適用する。

概要

高いパフォーマンスを求めるアプリケーションに適した共通言語ランタイム (CLR) を作るのに、多大な労力が費やされました。しかし、CLR の機能を生かすか無駄にするかは、マネージ コードの書き方次第です。この章では、最適なマネージ コードを作るために知っておくべき、パフォーマンスに関連する主要問題を特定します。また、よく見られるミスや、マネージ コードのパフォーマンスを向上させる数々の方法を紹介していきます。

この章では始めに、CLR アーキテクチャを説明した上で、マネージ コード作成時に知っておくべき、パフォーマンスとスケーラビリティに関する主要問題の概要を提示します。次に、(ビジネス ロジック、データ アクセス ロジック、ユーティリティ コンポーネント、Web ページ アセンブリを含む) すべてのマネージ コード作成に適用すべき、設計ガイドライン集を提供します。続く各節では、マネージ コード作成における、パフォーマンスに重大な影響を及ぼす各エリアについて、主な推奨事項を明示します。このエリアに該当するものとしては、メモリ管理、ガベージ コレクション、ボックス化処理、リフレクションと遅延バインディング、文字列処理、スレッド処理、並列処理、非同期処理、例外管理などがあります。

この章の使いかた

この章では、CLR アーキテクチャ、パフォーマンスとスケーラビリティに関する主な問題、マネージ コード作成のための設計ガイドライン集を示します。

  • 目的のトピックだけを参照するか、最初から最後まで読む - この章で掲げる大見出しを見れば、関心のあるトピックを見つけることができます。あるいは、この章を最初から最後まで読み、パフォーマンスとスケーラビリティに関する設計上の問題について、徹底した知識を得ることもできます
  • CLR のアーキテクチャとコンポーネントについて知る - マネージ コードの実行についての理解は、パフォーマンスにとって最適なコードを書く上で手助けとなります。
  • パフォーマンスとスケーラビリティに関する主な問題を知る - この章の「パフォーマンスとスケーラビリティに関する問題」を読み、マネージ コードのパフォーマンスとスケーラビリティに影響を及ぼし得る、主な問題について学んでください。これらを理解することは、パフォーマンスとスケーラビリティに関する問題を効果的に特定し、この章で提供する推奨事項を適用する上で重要です。
  • アプリケーションのパフォーマンスを計測する - 第 15 章の「.NET アプリケーション パフォーマンスの計測」の「CLR とマネージ コード」、および「.NET フレームワーク テクノロジ」を読み、アプリケーションのパフォーマンスの計測に使用できる主なメトリクスについて学んでください。アプリケーションのパフォーマンスを計測できるようになることは、パフォーマンス問題を正確にとらえる上で重要です。
  • アプリケーションのパフォーマンスをテストする - 第 16 章「.NET アプリケーション パフォーマンスのテスト」を読み、アプリケーションのパフォーマンス テストを行なう方法を学んでください。一貫したテスト プロセスを用い、結果を分析することは重要です。
  • アプリケーションのパフォーマンスをチューニングする - 第 17 章「.NET アプリケーション パフォーマンスのチューニング」の「CLR のチューニング」を読み、チューニング メトリクスの使用によって特定したパフォーマンス問題の解決方法を学んでください。
  • このガイドの「チェックリスト」の節を利用する - 「チェックリスト: マネージ コード パフォーマンス」を利用してください。この章で示すガイドラインについて素早く確認し、評価することができます。

アーキテクチャ

CLR は、マネージ コードの実行にかかわる、数多くのコンポーネントで構成されています。これらのコンポーネントは、この章のいたるところに出てきます。そのため、それぞれの目的について、あらかじめ知っておくべきです。図 5.1 は、CLR の基本的なアーキテクチャとコンポーネントを表したものです。

ms998547.ch05_clr-architecture(ja-jp,MSDN.10).gif

図 5.1 CLR アーキテクチャ

マネージ コードの書き方は、図 5.1 で示した CLR コンポーネントの効率に大きな影響を及ぼします。この章で提示するガイドラインと技法に従えば、コードを最適化し、動作時におけるコンポーネントの効率を最大限に高めることができます。以下は、各コンポーネントの目的をまとめたものです。

  • JIT コンパイラ - ジャスト イン タイム (JIT) コンパイラは、アセンブリの Microsoft 中間言語 (MSIL) を、動作時にネイティブ マシン コードに変換します。呼び出されることのないメソッドは、 JIT でコンパイルされません。
  • ガベージ コレクタ - メモリの割り当て、開放、圧縮を行います。
  • 構造化例外処理 - ランタイムは構造化例外処理をサポートし、堅牢で保守しやすいコードの作成を可能にしています。Try / catch / finally などの言語構造を使用し、構造化例外処理を有効利用できるようにしてください。
  • スレッド処理 - .NET Framework は数多くのスレッド処理プリミティブや同期プリミティブを提供し、パフォーマンスの高いマルチスレッド コードの作成を可能にしています。スレッド処理アプローチと同期メカニズムの選択は、アプリケーションの並行処理性に影響を与えます。したがって、スケーラビリティと全体のパフォーマンスにも影響します。
  • セキュリティ - .NET Framework は、コード アクセス セキュリティを提供しています。ファイル システムへのアクセス、アンマネージ コードの呼び出し、ネットワーク リソースへのアクセス、レジストリへのアクセスなど、特定の処理の実行について、コードは許可を要求されます。
  • ローダ - .NET Framework ローダは、アセンブリを見つけ出してロードします。
  • メタデータ - アセンブリは、自己記述的です。アセンブリには、提供する型のセットや、それらの型が含むメンバなどのプログラム情報を示す、メタデータが含まれています。メタデータは、 JIT コンパイルを容易にし、バージョン情報やセキュリティ関連情報の提供にも利用されます。
  • 相互運用 - CLRは、Microsoft Visual BasicR、 Microsoft Visual C++R、DLL、COM コンポーネントなど、各種アンマネージ コードとの相互運用が可能です。相互運用により、これらのアンマネージ コードを、マネージ コードで呼び出すことができます。
  • リモート処理 - .NET リモート処理インフラストラクチャは、アプリケーション ドメイン、プロセス、各種ネットワーク トランスポートをまたいだ呼び出しをサポートします。
  • デバッグ - CLR は、アセンブリのデバックやプロファイルに使用できるデバッグ フックを提供しています。

パフォーマンスとスケーラビリティに関する問題

ここでは、マネージ コードのパフォーマンスとスケーラビリティに影響を及ぼし得る主な問題について、一通り説明します。この章の以降の節では、ここで掲げる問題を予防または解決するための方策、ソリューション、技術的推奨事項を提示します。まずは以下に、マネージ コードのパフォーマンスとスケーラビリティに影響を及ぼす問題を示します。

  • メモリの誤使用 - オブジェクトを多く作りすぎた場合、リソースを適切に開放しなかった場合、メモリをプリアロケートした場合、ガベージ コレクションを明示的に強制実行した場合は、CLR によるメモリ管理の効率性が損なわれることがあります。これは、ワーキング セット サイズの増加や、パフォーマンスの低下につながりかねません。
  • リソースのクリーンアップ - 不必要なファイナライザの実装、Dispose メソッドでのファイナライゼーションの許可、アンマネージ リソースの放置により、リソースの再確保は必要以上に遅れかねず、場合によってはリソース漏れが発生します。
  • スレッドの不適切な使用 - 要求ごとにスレッドを作成することや、スレッド プールを利用しているスレッドを共有しないことにより、サーバー アプリケーションでパフォーマンス / スケーラビリティ上のボトルネックが発生する場合があります。.NET Frameworkは、サーバー側アプリケーションが使用すべき、セルフ チューニング スレッド プールを提供しています。
  • 共有リソースの乱用 - 要求ごとにリソースを作成すると、リソース消費量は非常に大きくなり得ます。また、共有リソースを正しく開放しないと、再確保が遅れることになりかねません。これらは、スケーラビリティ上の問題に直結します。
  • 型変換 - 暗黙の型変換や値型と参照型の混在は、過度のボックス化 / ボックス化解除処理につながります。これは、パフォーマンスにも影響します。
  • コレクションの誤使用 - .NET Framework クラス ライブラリは、コレクション型の拡張セットを提供しています。各コレクション型は、特定の格納要件やアクセス要件の下で使われることになっています。誤ったコレクション型を選択すると、状況によってはパフォーマンスが低下します。
  • 非効率なループ - どれほど小さなコーディングの非効率性も、ループに入っていると無視できないものになります。オブジェクトのプロパティにアクセスするループは、パフォーマンス ボトルネックの原因となることがよくあります。オブジェクトがリモートにある場合や、プロパティ ゲッタが大きな作業を実行する場合は、特に注意すべきです。

設計上の考慮事項

アプリケーションのパフォーマンスに最も大きく影響するのは、アーキテクチャと設計です。パフォーマンスを、アプリケーションの開発ライフ サイクルを通して考慮すべき機能要件と考え、設計とテストを進めてください。アプリケーションの展開は、反復的なプロセスとするべきです。パフォーマンスのテストと計測は、反復中に行うべきで、展開時になってから行うべきものではありません。

ここでは、マネージ コード設計時に考慮すべき、設計上の主な考慮事項について、概略を示します。

  • 効率的なリソース管理を心掛けて設計する。
  • 境界越えを減らす。
  • 複数の小さなアセンブリよりも 1 つの大きなアセンブリを作る。
  • 論理階層によってコードを区画化する。
  • スレッドを共有リソースとして扱う。
  • 効率的な例外管理を心掛けて設計する。

効率的なリソース管理を心掛けて設計する

カプセル化されたオブジェクトやリソースを、必要となる前から割り当てないようにしてください。また、関連コードが完了したら、それらを速やかに開放するようにしてください。このアプローチは、データベース コネクション、データ リーダー、ファイル、ストリーム、ネットワーク コネクション、COM オブジェクトなど、すべてのリソース タイプに適用可能です。finally ブロックや Microsoft Visual C#R の using ステートメントを使い、リソースのクローズとリリースが、例外状況下でも適切なタイミングで行われるようにしてください。C# の using ステートメントは、IDisposable を実装したリソースについてのみ、使用可能なことに注意してください。一方、finally ブロックは、いかなるタイプのクリーンアップ処理でも使用可能です。

境界越えを減らす

リモート処理の境界を越えるメソッド呼び出しの数を減らすようにしてください。これは、マーシャリングや、場合によってはスレッド切り替えのオーバーヘッドが発生するためです。マネージ コードについては、考慮すべき境界がいくつかあります。

  • アプリケーション ドメイン越え - 単一プロセスのコンテキスト内となるため、最も効率的な境界越えです。実際の呼び出しコストは非常に低いため、オーバーヘッドは、メソッド呼び出しで渡される引数の数、型、サイズにより、ほぼ完全に決まります。
  • プロセス境界越え - プロセス境界を越えると、パフォーマンスに大きく影響します。プロセスを越える呼び出しは、絶対に必要な場合にのみ、実行してください。具体例として、セキュリティまたは障害耐性を理由に、Enterprise Services サーバー アプリケーションが必要となる場合が挙げられます。プロセス越えが必要となる場合でも、パフォーマンスとの間でバランスをとるようにしてください。
  • マシン境界越え - マシン境界越えは、ネットワーク待ち時間とマーシャリング オーバーヘッドにより、境界越えの中でも最も大きな負担を発生させます。マーシャリング オーバーヘッドは、あらゆる境界越えで発生しますが、マシン境界越えでは特に大きくなり得ます。例えば、HTTP プロキシの導入により、SOAP エンベロープの使用が必要となり、追加的なオーバーヘッドが発生します。設計でリモート サーバーを採用する前に、パフォーマンス、セキュリティ、管理などの間におけるバランスを考慮する必要があります。
  • アンマネージ コード - アンマネージ コードの呼び出しについても、考慮する必要があります。アンマネージ コードの呼び出しは、マーシャリング オーバーヘッドや、場合によってはスレッド切り替えオーバーヘッドを発生させます。CLR の Platform Invoke (P/Invoke) レイヤや COM 相互運用レイヤは、非常に効率的です。しかし、マネージ コードとアンマネージ コードの間でマーシャリングを必要とするデータの型とサイズにより、パフォーマンスは大きく変動します。詳細は、第 7 章「相互運用パフォーマンスの向上」を参照してください。

複数の小さなアセンブリよりも 1 つの大きなアセンブリを作る

アプリケーションのワーキング セットを減らすためには、複数の小さなアセンブリよりも、1 つの大きなアセンブリを作るべきです。常にまとめてロードするアセンブリが複数ある場合、それらを結合し、1 つのアセンブリとするべきです。

複数の小さなアセンブリを持つことによるオーバーヘッドは、以下によって発生します。

  • 小さなアセンブリについてのメタデータのロードによるコスト
  • アセンブリをロードするための、CLRのコンパイル前イメージ内の各種メモリ ページに対する接触 (Ngen.exe でプリコンパイルされる場合)
  • JIT コンパイル時間
  • セキュリティ チェック

コストは、プログラムがアクセスするメモリ ページについてのみ、発生します。大きなアセンブリは、生成されるネイティブ イメージの最適化を実現しやすい、Native Image Generator ユーティリティ (Ngen.exe) を提供します。イメージ レイアウトの向上とは、具体的には、必要なデータを凝集してレイアウトすることです。この結果、ジョブを行うのに必要な全体のページ量は、複数のアセンブリでレイアウトされた同じコードに比べ、少なくなります。

アセンブリの分割を避けられない場合もあります。例えば、バージョン管理や展開の都合により、不可避となる場合です。型を分けて提供する必要があれば、アセンブリも分けなければならない場合もあります。

論理階層によってコードを区画化する

内部のクラス設計を確認し、コードがどのように各メソッドに区画化されているかを調べてください。コードが適切に区画化されていれば、パフォーマンス向上のためのチューニング、保守、新しい機能の追加が容易になります。しかし、バランスをとる必要があります。明確に区画化されたコードは、保守性を向上させる一方、過度の抽象化やレイヤを増やしすぎてしまうことに注意すべきです。シンプルな設計ほど、効果と効率は高くなります。

スレッドを共有リソースとして扱う

スレッドを要求ごとに作成しないでください。スケーラビリティに大きな影響を及ぼし得るためです。新しいスレッドの作成は、大きな負担を伴う処理であり、この負担は最小化すべきです。スレッドを共有リソースとして扱うと共に、最適化した .NET スレッド プールを利用してください。

効率的な例外管理を心掛けて設計する

例外のスローは、大きなパフォーマンス コストを発生させます。エラー状態への対応の推奨方法は構造化例外処理ですが、エラー状態が発生した例外的な状況においてのみ、例外を用いるようにしてください。例外を正規の制御フローの一部としてはなりません。

クラス設計に関する考慮事項

クラス設計における判断は、システムのパフォーマンスとスケーラビリティに大きな影響を与える場合があります。ただし、これらを追求するだけでなく、機能性や保守性などとのトレードオフ、そして会社のコーディング方針を分析する必要あります。

ここでは、マネージ クラスの設計についてのガイドラインを示します。

  • クラスをデフォルトでスレッド セーフとしない。
  • sealed キーワードの使用を検討する。
  • 仮想メンバのトレードオフを考慮する。
  • オーバーロード メソッドの使用を検討する。
  • 値型について Equals メソッドのオーバーライドを検討する。
  • プロパティへのアクセスに必要なコストを知っておく。
  • プライベート メンバ変数とパブリック メンバ変数を対比評価する。
  • 揮発性フィールドの使用を限定する。

クラスをデフォルトでスレッド セーフとしない。

個々のクラスをスレッド セーフにする必要があるかについて、注意深く考慮してください。ソフトウェア アーキテクチャの上位レイヤでスレッド セーフティと同期が必要でも、個々のクラス レベルでは必要とならない場合は、よくあります。クラスの設計時に適切な原子性レベルが分からない状況は、特に低いレベルのクラスでは考えられます。

例えば、スレッド セーフなコレクション クラスについて考えてみてください。クラスが、別のクラスやカウント変数など、原子性レベルの違うものに更新されると同時に、組み込まれたスレッド セーフティは無用になります。スレッド制御は、もっと上のレベルで必要になります。この状況には、2 つの問題があります。まず、クラスが提供するスレッド セーフティ特性によるオーバーヘッドは、スレッド セーフティ自体が不要になっても残り続けます。次に、コレクション クラスの設計は、これらのスレッド セーフティ サービスを提供するために複雑になります。この結果、クラス使用時に毎回、代償を支払わなければならなくなります。

通常のクラスとは対照的に、静的クラス(静的メソッドのみを持つクラス)は、デフォルトでスレッド セーフとするべきです。静的クラスはグローバルな状態情報しか保持しておらず、通常は、その共有状態情報の初期化と管理を、プロセスを通して行うためのサービスを提供します。このためには、適切なスレッド セーブティが必要となります。

sealed キーワードの使用を検討する

sealed キーワードを、クラス レベルとメソッド レベルで使用できます。Visual Basic .NET では、NotInheritable キーワードをクラス レベルで、NotOverridable をメソッド レベルで使用できます。手元の基本クラスからの拡張を他者にしてほしくない場合は、それらのクラスに sealed キーワードを付けるべきです。クラス レベルで sealed キーワードを使う前に、拡張性要件を慎重に確認する必要があります。

仮想メンバを持つ基本クラスから作った派生クラスの機能を、他者に拡張してほしくない場合は、その派生クラスの仮想メンバのシールを検討できます。シールされた仮想メソッドは、インライン化など、コンパイラ最適化の対象となります。

以下の例について考えてみてください。

public class MyClass{
protected virtual void SomeMethod() { ... }
}

派生クラスでメソッドをオーバーライドし、シールすることができます。

public class DerivedClass : MyClass {
protected override sealed void SomeMethod{ ... }
}

このクラスは一連の仮想オーバーライドで終わり、DerivedClass.SomeMethod をインライン化の対象にしています。

詳細情報

Visual Basic .NET における継承についての詳細は、MSDNR Magazine の Ted Pattison 著 "Using Inheritance in the .NET World, Part 2" を参照してください。

仮想メンバのトレードオフを考慮する

拡張性を高めるためには、仮想メンバを使ってください。クラス設計を拡張する必要がないなら、仮想メンバは使わないようにしてください。仮想テーブル検索による呼び出しの負担が大きく、実行時のパフォーマンス最適化を妨げる場合があるためです。例えば、仮想メンバは、コンパイラでインライン化できません。また、サブタイプを許可している場合、コンシューマに提供するコントラクトが非常に複雑になり、後になってクラスのアップグレードを図る場合に、バージョン管理の問題を避けて通れなくなります。

オーバーロード メソッドの使用を検討する

引数が変化する場合は、センシティブな(可変数の引数に対応する)メソッドの代わりに、オーバーロード メソッドを使用することを検討してください。センシティブなメソッドを使うと、起こり得るすべての引数の組み合わせについて、特別なコード パスが必要となります。

// 可変数の引数に対応するメソッド
void GetCustomers (params object [] filterCriteria)
// オーバーロード メソッド
void GetCustomers (int countryId, int regionId)
void GetCustomers (int countryId, int regionId, int CustomerType)

メモ: .NET コンポーネントにアクセスする COM クライアントが存在する場合は、オーバーロード メソッドは有効となりません。代わりに、異なる名前のメソッドを使用してください。

値型について Equals メソッドのオーバーライドを検討する

値型について Equals メソッドをオーバーライドし、そのパフォーマンスを向上させることができます。Equals メソッドは、System.Object が提供しています。Equals の標準実装を使うには、値型をボックス化し、参照型 System.ValueType のインスタンスとして渡さなければなりません。これにより、Equals メソッドはリフレクションを用いて比較を実行します。しかし、変換とリフレクションに伴うオーバーヘッドは、実行を要する比較の実質的コストを簡単に超えてしまいかねません。つまり、各値型に固有の Equals メソッドで比較を実行した方が、負担が大幅に小さくなる場合もあるのです。

以下のコードでは、Equals メソッドをオーバーライドしてリフレクション コストを避けることにより、パフォーマンスを向上させています。

public struct Rectangle{
public double Length;
public double Breadth;
public override bool Equals (object ob) {
if(ob is Rectangle)
return Equals((Rectangle)ob);
else
return false;
}
private bool Equals(Rectangle rect) {
return this.Length == rect.Length && this.Breadth==rect.Breadth;
}
}

プロパティへのアクセスに必要なコストを知っておく

プロパティはフィールドと似たものに見えますが、違うもので、隠れたコストを含んでいる場合があります。パブリック フィールド、または パブリック プロパティを使用することにより、クラス レベルのメンバ変数を公開できます。プロパティを使用すると、オブジェクト指向プログラミングのメリットを得られます。これは、プロパティの使用により、検証とセキュリティ チェックをカプセル化し、プロパティへのアクセス時にそれらが実行されるようにすることができるためです。しかし、見た目がフィールドと変わらないため、しばしば誤用されることがあります。

プロパティにアクセスすると、検証ロジックなどの付加的なコードが実行される可能性があることを、頭に置いておくべきです。つまり、プロパティへのアクセスは、フィールドへの直接的なアクセスに比べ、遅くなる場合があるということです。ただし、この付加的なコードというのは、普通は正当な理由によって存在しています。例えば、有効なデータのみを許可するために用いられます。

(プライベート メンバ変数の直接的な設定や取得以外に) 付加的なコードを持たないシンプルなプロパティであれば、コンパイラによるプロパティ コードのインライン化が可能になるため、パブリック フィールドへのアクセスと比較したパフォーマンス上の差異はなくなります。しかしプロパティは、いつもそれほどシンプルなわけではありません。例えば、仮想プロパティはインライン化できません。

リモート アクセスを考慮してオブジェクトを作ったなら、クライアントに複数のプロパティまたはフィールドの設定を要求するよりも、複数の引数を受けるメソッドを使用するべきです。これにより、ラウンド トリップ数を減らすことができます。

複雑なビジネス ルールなどのコストのかかる処理を隠すためにプロパティを使用するのは、極めて好ましくありません。プロパティはコストが小さいものという想定が、呼び出し元にあるためです。クラスは、コストに相応の方法で設計してください。

プライベート メンバ変数と パブリック メンバ変数を対比評価する

可視性についての考慮に加え、パブリック メンバを必要以上に使わないようにしてください。XmlSerializer クラスを使用すると、デフォルトではすべての パブリック メンバをシリアル化するため、付加的なシリアル化オーバーヘッドが発生することになるためです。

揮発性フィールドの使用を制限する

揮発性フィールドはコンパイラによるフィールド内容の読み書きを制限するため、volatile キーワードの使用を限定してください。volatile キーワードを使用すると、コンパイラは、フィールドの値がロードされている可能性のあるレジスタではなく、常にフィールドのメモリ位置を読みに行くコードを生成します。つまり、システムはレジスタでなくメモリ アドレスを使用することを強いられるため、揮発性フィールドへのアクセスは、不揮発性フィールドへのアクセスに比べて遅くなります。

実装に関する考慮事項

設計に入ったら、マネージ コードの作成についての技術詳細を考慮しなければなりません。パフォーマンスを向上させるためには、CLR を有効利用する必要があります。マネージ コードのパフォーマンスの主な指標値としては、応答時間、スループット速度、リソース管理などがあります。

応答時間は、パフォーマンスに大きく影響するコード パスを最適化し、ガベージ コレクタによる効率的なメモリ開放を可能にするコードを書くことにより、改善可能です。アプリケーションのアロケーション プロファイルを分析すれば、ガベージ コレクションのパフォーマンスを向上させることができます。

スループットは、スレッドの効果的な使用によって改善できます。スレッド作成を最小限に抑え、スレッド プールを使用して高くつくスレッドの初期化を避けてください。パフォーマンスに大きく影響するコードには、リフレクションや遅延バインディングを入れないようにするべきです。

リソース利用度は、ファイナライゼーションの効果的な使用 (Dispose パターンの使用) を通してアンマネージ リソースを開放することにより、改善可能です。また、文字列、配列、コレクション、ループ構造を効果的に用いることによっても、改善できます。ロックと同期は慎重に使用するようにし、使う場合でも、ループ期間を最小限に抑えるべきです。

以下では、マネージ コード開発時におけるパフォーマンス上の考慮事項を、詳しく説明していきます。

ガベージ コレクションについての説明

.NET Framework では、すべてのアプリケーションについて、メモリ管理で自動ガベージ コレクションを使用しています。new 演算子を用いてオブジェクトを作成すると、マネージ ヒープでメモリが確保されます。ガベージ コレクタは、不要なメモリが十分に溜まったと判断すると、コレクションを実行してメモリの一部を開放します。このプロセスは完全に自動化されていますが、効率化を図るために留意すべき要素が、いくつか存在します。

ガベージ コレクションの原理を理解するには、マネージ オブジェクトのライフ サイクルを理解する必要があります。

  1. new を呼び出すと、マネージ ヒープにオブジェクト用のメモリが割り当てられます。メモリの割り当ての後、オブジェクトのコンストラクタが呼び出されます。
  2. オブジェクトが一定期間にわたって使われます。
  3. すべての参照が明示的に null に設定されるか、適用範囲外となることにより、オブジェクトはガベージとなります。
  4. その後、オブジェクトのメモリは開放 (回収) されます。通常、他のオブジェクトは、開放されたメモリを再利用できます。

アロケーション

マネージ ヒープは、連続したメモリのブロックと考えることができます。オブジェクトを新しく作成すると、メモリはマネージ ヒープ内の次の利用可能場所に割り当てられます。ガベージ コレクタがスペースを探す必要はないため、利用可能なメモリが十分にあれば、割り当ては極めて速やかに行われます。新しいオブジェクトに対応するだけのメモリが無い場合は、ガベージ コレクタがスペース確保のために働きます。

コレクション

スペースを確保するために、ガベージ コレクタは参照不能となっているオブジェクトを回収します。オブジェクトへの参照が無い場合、すべての参照が null にセットされている場合、そしてオブジェクトへのすべての参照が、現在のコレクション サイクル中に回収対象となる他のオブジェクトに基づいている場合に、オブジェクトは参照不能とみなされます。

コレクション実行時に、参照可能なオブジェクトは、トレース済みとしてマークされます。ガベージ コレクタは、それらの参照可能なオブジェクトを連続したスペースに動かした上で、参照不能になったオブジェクトが使っていたスペースを確保します。残ったオブジェクトは、次のジェネレーションへ移ります。

ジェネレーション

ガベージ コレクタは、ライフ タイムと揮発性によってオブジェクトを 3 グループに分けます。

  • ジェネレーション 0 (Gen 0) - 新しく作成されたオブジェクトから成ります。Gen 0 に対するコレクションは頻繁に行われ、ライフ タイムの短いオブジェクトは速やかにクリーンアップされます。残ったオブジェクトは、ジェネレーション 1へ移ります。
  • ジェネレーション 1 (Gen 1) - Gen 0 ほど頻繁にコレクションの対象とならず、Gen 0 から移ってきた比較的ライフ タイムの長いオブジェクトから成ります。
  • ジェネレーション 2 (Gen 2) - Gen 1 から移ってきたオブジェクト (つまり最もライフ タイムの長いオブジェクト) から成り、コレクションの頻度はさらに下がります。ライフ タイムの長いオブジェクトほど、回収や移動の頻度を低くするというのが、ガベージ コレクタの基本方針です。

主なGC メソッドについての説明

System.GC クラスの主なメソッドを、表 5.1 で示します。このクラスを使うことにより、ガベージ コレクタの動作を制御できます。

表 5.1: 主な GC メソッド

メソッド名説明
System.GC.Collectガベージ コレクションを強制実行します。原則として、このメソッドの使用は避け、コレクション実行のタイミングはランタイムに任せるべきです。このメソッドが呼び出されることが多いのは、開放されたはずのメモリが、開放されないままになっているように感じる場合でしょう。しかし、その状況は主として、不要なメモリを保持し続けることによってもたらされます。この場合は、コレクションを強制実行しても、効果は得られません。
System.GC.WaitForPendingFinalizersファイナライゼーション スレッドがファイナライゼーション キューを空にするまで、現在のスレッド処理を中断します。原則として、このメソッドは System.GC.Collect の直後に呼び出され、全オブジェクトに対するファイナライザが呼び出されるまで、現在のスレッドを待機させます。しかし、GC.Collect を呼び出すべきではないため、GC.WaitForPendingFinalizers を呼び出す必要もありません。
System.GC.KeepAliveオブジェクトへの参照を保持することにより、まだ破棄すべきでないオブジェクトが回収されてしまうのを防ぐために使われます。このメソッドが使われることが多いのは、マネージ コードにはオブジェクトへの参照が無いものの、アンマネージ コードでは使われている場合です。
System.GC.SuppressFinalize特定のオブジェクトをファイナライザから守るために使われます。Dispose パターンを実装している場合は、このメソッドを使ってください。クライアントからオブジェクトの Dispose メソッドの呼び出しを受けたためにリソースを明示的に開放した場合、ファイナライゼーションの必要はないため、Dispose は SuppressFinalize を呼び出すべきです。

サーバー GC vs. ワークステーション GC

CLR は、2 つのガベージ コレクタを提供しています。

  • ワークステーション GC (Mscorwks.dll) - Windows Forms アプリケーションなどのデスクトップ アプリケーションを対象としています。
  • サーバー GC (Mscorsvr.dll) - サーバー アプリケーションを対象としています。ASP.NETは、サーバー GC をロードします。ただし、プロセッサが複数個ある場合に限ります。シングル プロセッサ サーバーには、ワークステーション GC をロードします。

メモ: 本稿執筆の時点で、.NET Framework 2.0 では、2 つの GC が共に Mscorwks.dll に含まれており、Mscorsvr.dll はなくなっています。

サーバー GC は、スループット、メモリ消費、マルチプロセッサ スケーラビリティについて最適化されています。一方、ワークステーション GC は、デスクトップ アプリケーション向けにチューニングされています。サーバー GC 使用時は、マネージ ヒープが複数のセクションに分割されます。マルチプロセッサ コンピュータでは、CPU と同じ数に分割されます。コレクションを開始すると、コレクタは CPU ごとに 1 つのスレッドを持ちます。各スレッドは、それぞれのセクションでコレクションを同時に行います。ワークテーション向けの実行エンジン (Mscorwks.dll) は、待ち時間がより短くなるように最適化されています。ワークステーション GC は、CLR スレッドと並行してコレクションを実行します。サーバー GC は、コレクション中は CLR スレッドの処理を中断します。

マルチプロセッサ コンピュータで独自アプリケーションをホストしている場合に、サーバー GC の機能が必要となるかもしれません。例えば、.NET リモート処理のホストを使い、マルチプロセッサ サーバー上に展開されている Windows サービス のために、サーバー GC 機能が必要となる場合が考えられます。この場合は、CLR とサーバー GC タイプのガベージ コレクタをロードする、独自のホストを作る必要があります。このアプローチについての詳細は、MSDN Magazine の Steven Pratschnearticle 著 "Microsoft .NET: Implement a Custom Common Language Runtime Host for Your Managed App" を参照してください。

メモ: 本稿執筆の時点で、.NET Framework 2.0 は、アプリケーションのコンフィギュレーションによるサーバー GC と ワークステーション GC の切り替えをサポートしています。

ガベージ コレクションに関するガイドライン

ここでは、ガベージ コレクションのパフォーマンスを向上させるための推奨事項をまとめます。

  • アプリケーションのアロケーション プロファイルを特定して分析する。
  • GC.Collect の呼び出しを避ける。
  • キャッシュ データ について弱い参照の使用を検討する。
  • ライフ サイクルの短いオブジェクトを昇格させない。
  • 所要時間の長い呼び出しをする前に不要なメンバ変数を null に設定する。
  • 隠れたアロケーションを最小限に抑える。
  • 複雑なオブジェクト グラフを避けるか最小限に抑える。
  • メモリのプリアロケートとチャンクを避ける。

アプリケーションのアロケーション プロファイルを特定して分析する

オブジェクト サイズ、オブジェクト数、オブジェクト ライフ タイムはすべて、アプリケーションのアロケーション プロファイルに影響を及ぼす要素です。アロケーションは素早く行われますが、ガベージ コレクションの効率は、(特に) 回収対象のジェネレーションによって変わります。Gen 0 の小さなオブジェクトを回収するのが、最も効率的です。Gen 0 は最も小さく、通常は CPU キャッシュに適合するためです。対照的に、Gen 2 のオブジェクトを頻繁に回収すると、負担は大きくなります。アロケーションの実行タイミングと、対象ジェネレーションを特定するには、CLR プロファイラなどのアプリケーション プロファイラを使い、アプリケーションのアロケーション パターンを確認してください。

詳細は、このガイドの「How To 情報」にある「CLR プロファイラの使用方法」を参照してください。

GC.Collect の呼び出しを避ける

デフォルトの GC.Collect メソッドは、すべてのジェネレーションを対象にコレクションを実行します。この場合、コレクションを完了させるには、システム内に存続している、文字通りすべてのオブジェクトに接触しなければならなくなるため、大きな負担が発生します。もちろん、通常は時間も非常にかかってしまいます。一方、ガレージ コレクタのアルゴリズムは、コストに見合う必要があると判断したときに限り、全面的コレクションを実行するように調整されています。そのため、GC.Collect を直接呼び出すのは、避けてください。実行のタイミングは、ガベージ コレクタに任せるようにしてください。

ガベージ コレクタには、セルフ チューニング機能が備わっており、アプリケーションのニーズを満たすように、メモリ使用量に基づいて処理を調整します。プログラムによってコレクションを強制実行すると、ガベージ コレクタのチューニングとオペレーションが損なわれる場合があります。

GC.Collect を呼び出さなければならない特別な事情がある場合は、以下を参考にしてください。

  • GC.Collect 呼び出し後に GC.WaitForPendingFinalizers を呼び出します。これにより、すべてのオブジェクトに対するファイナライザが呼び出されるまで、現在のスレッドは待機します。
  • ファイナライザ作動後は、回収すべき、(ファイナライズされたばかりの) アクセス不可能なオブジェクトが増えます。もう1度 GC.Collect を呼び出し、それらを回収します。
    System.GC.Collect(); // アクセス不可能なオブジェクトを除去
    System.GC.WaitForPendingFinalizers(); // ファイナライゼーションが終わるまでスレッド待機
    System.GC.Collect(); // ファイナライズされたばかりのオブジェクトに関連するメモリを開放
    

キャッシュ データについて弱い参照の使用を検討する

キャッシュ データを扱っている場合は、弱い参照の使用を検討してください。これにより、キャッシュ オブジェクトについて、必要な場合に簡単に復活させることや、メモリ使用量が大きい場合にガベージ コレクションによって開放することが可能になります。弱い参照は主に、サイズの小さくないオブジェクトについて使用すべきです。これは、弱い参照自体がオーバーヘッドを伴うものであるためです。弱い参照は、コレクション内に格納された、サイズが中から大のオブジェクトに適しています。

アプリケーションで、従業員情報についての独自のキャッシング ソリューションを使用している場合について考えてみてください。WeakReference ラッパーを介してオブジェクトを保持していれば、メモリ負担の増加期間にメモリ使用量が高まると、オブジェクトは回収されます。

その後のキャッシュ参照でオブジェクトを見つけられなければ、正式な永続的ソースに格納された情報から再生してください。このようにして、キャッシュの使用と永続的な媒体の使用との間で、バランスをとってください。

void SomeMethod()
{
// コレクションを作成
ArrayList arr = new ArrayList(5);
// カスタム オブジェクトを作成
MyObject mo = new MyObject();
// カスタム オブジェクトから弱い参照オブジェクトを作成
WeakReference wk = new WeakReference(mo);
// コレクションに弱い参照オブジェクトを追加
arr.Add(wk);
// 弱い参照を取得
WeakReference weakReference = (WeakReference)arr[0];
MyObject mob = null;
if( weakReference.IsAlive ){
mob = (MyOBject)weakReference.Target;
}
if(mob==null){
// ガベージ コレクタに回収されたオブジェクトを再生
}
// オブジェクトが存在するため処理を継続
}

ライフ タイムの短いオブジェクトを昇格させない

Gen 0 のうちに割り当てられ、回収されるオブジェクトを、ライフ タイムの短いオブジェクト と呼びます。以下の原則により、ライフ タイムの短いオブジェクトが上のジェネレーションに昇格するのを防ぐことができます。

  • ライフ タイムの長いオブジェクトからライフ タイムの短いオブジェクトを参照しない - これが当てはまりやすいのは、ローカル オブジェクトをクラス レベルのオブジェクト参照に割り当てる場合です。
    class Customer{
    Order _lastOrder;
    void insertOrder (int ID, int quantity, double amount, int productID){
    Order currentOrder = new Order(ID, quantity, amount, productID);
    currentOrder.Insert();
    this._lastOrder = currentOrder;
    }
    }
    

    このようなコードの作成は避けてください。オブジェクトが Gen 0 から昇格する可能性が高まり、オブジェクトのリソースの開放が遅れます。この問題を解決できる実装例を、以下に示します。

    class Customer{
    int _lastOrderID;
    void ProcessOrder (int ID, int quantity, double amount, int productID){
    . . .
    this._lastOrderID = ID;
    . . .
    }
    }
    

    指定の Order クラスを、必要に応じて ID によって取り出せます。

  • Finalize メソッドの実装を避ける - ガベージ コレクタは、ファイナライゼーションを容易にするため、ファイナライズ可能なオブジェクトを上のジェネレーションへ昇格させます。これにより、オブジェクトのライフ タイムは長くなります。
  • ファイナライズ可能なオブジェクトからの参照を避ける - 参照すると、参照先のオブジェクトのライフ タイムが長くなってしまいます。

詳細情報

ガベージ コレクションについての詳細は、以下の資料を参照してください。

所要時間の長い呼び出しをする前に不必要なメンバ変数を null に設定する

所要時間の長い呼び出しをブロックする前に、呼び出しに先立って不必要なメンバ変数を明示的に null にセットし、回収可能にすることを検討するべきです。このコードの例を、以下に示します。

class MyClass{
private string str1;
private string str2;
void DoSomeProcessing(...){
str1= GetResult(...);
str2= GetOtherResult(...);
}
void MakeDBCall(...){
PrepareForDBCall(str1,str2);
str1=null;
str2=null;
// データベース (または所要時間の長い) 呼び出しをする
}
}

このアプローチは、静的または辞書式検索的にはアクセス可能でも、事実上不要となったあらゆるオブジェクトに適用できます。

  • 自分のクラス、または他のクラスで不要になった静的変数を null に設定してください。
  • 可能な場合、状態情報を削減するのも、有効な手段です。例えば、所要時間の長い呼び出しの前に、ツリーのほとんどを破棄可能かもしれません。
  • 所要時間の長い呼び出しの前に破棄可能なオブジェクトがあれば、それらを null に設定してください。

ローカル変数を null (C#) または Nothing (Visual Basic .NET) に設定しないでください。変数は参照されなくなり、明示的に null に設定する必要はないと、JIT コンパイラが静的に判断する場合があるためです。ローカル変数を使っているコードの例を、以下に示します。

void func(...)
{
String str1;
str1="abc";
// これを避ける
str1=null;
}

隠れたアロケーションを最小限に抑える

メモリ アロケーションは、新しいオブジェクト用のスペースを作るためにポインタが再配置されるだけなので、極めて短時間で行なわれます。しかし、いつかはメモリについてガベージ コレクションを実行しなければならず、これによってパフォーマンスは低下しかねません。そのため、単純に見えても、多くのアロケーションを発生させるコードには注意してください。例えば、String.Split は、文字列から文字列の配列を作るために、区切り記号を使用しています。これにより、String.Split は、区切られた各文字列について、新しい文字列オブジェクトを割り当てます。この結果、String.Split を (ソート ルーチンなどの) 重いコンテキストで使うと、負担は大きくなりかねません。

string attendees = "bob,jane,fred,kelly,jim,ann";
// 次の 1 行で 6 つのサブコードに割り当て、
// 出席者配列を出力して区切り配列を入力する
string[] names = attendees.Split( new char[] {','});

+= 演算子を使った文字列連結など、ループ内で発生するアロケーションにも注意してください。さらに、ハッシュ メソッドや比較メソッドでのアロケーションは、特に好ましくありません。検索やサーチのコンテキスト内で、繰り返し呼び出されることが多いためです。文字列の効率的な取り扱いについての詳細は、この章で後述する「文字列処理」を参照してください。

複雑なオブジェクト グラフを避けるか最小限に抑える

他のオブジェクトへの参照を多数含むような、複雑なデータ構造やオブジェクトを使用しないようにしてください。これらの割り当ては、大きな負担を伴い、ガベージ コレクタの作業を増やす場合があります。シンプルなグラフほど、ローカル性に優れ、保守に必要なコードも少なくて済みます。よく見られる誤りに、グラフを一般化しすぎてしまうというミスがあります。

メモリのプリアロケートとチャンクを避ける

C++ プログラマーはよく、メモリの大きなブロックを (mallocを使って) 割り当て、malloc を複数回呼び出さないように 1 度にチャンクします。しかし、マネージ コードの作成においては、以下の理由により、これは不適切です。

  • マネージ メモリのアロケーションは短時間で行われ、ガベージ コレクタはアロケーションが高速化されるように最適化されている。アンマネージコードでは、主にアロケーションを高速化するためにメモリをプリアロケートしますが、マネージ コードでは必要ありません。
  • メモリをプリアロケートすると、アロケーションが必要以上に発生する。これは、不要なガベージ コレクションの原因となり得ます。
  • ガベージ コレクタは、マニュアルでリサイクルされるメモリを開放できない。
  • プリアロケートされたメモリは、最終的に開放されるまでに長い時間を要し、リサイクル コストも増大させてしまう。

Finalize と Dispose についての説明

ガベージ コレクタは、ファイナライゼーションと呼ばれる、付加的なオプションを提供しています。クリーンアップ処理の実行を必要とするオブジェクトについて、コレクション処理中で、オブジェクトのメモリを回収する直前に、ファイナライゼーションを使ってください。ファイナライゼーションは、オブジェクトが保持するアンマネージ リソースを開放するために、最もよく使われます。その他で使用する場合は、よく注意する必要があります。アンマネージ リソースの例としては、ファイル ハンドル、データベース コネクション、COM オブジェクト参照などがあります。

Finalize

アンマネージ リソースを使用するオブジェクトは、これを開放するために、付加的なクリーンアップがを必要とします。これに対応するのが、ファイナライゼーションの役目です。オブジェクトは、Object.Finalize メソッドをオーバーライドすることにより、ファイナライゼーションを使用できるようになります。C# や C++ Managed Extensions では、C++ デストラクタのようなメソッドを提供することにより、Finalize を実装します。

メモ: Finalize メソッドの動作と C++ デストラクタの動作を混同すべきではありません。構文は似ていますが、似ているのはそこまでです。

オブジェクトの Finalize メソッドは、オブジェクトのマネージ メモリが回収される前に呼び出されます。これにより、オブジェクトが保持しているアンマネージ リソースを開放することができます。Finalize を実装している場合、メソッドを呼び出すタイミングは、ガベージ コレクタに任されているため、コントロールできません。これは通常、非確定的ファイナライゼーションと呼ばれています。

ファイナライゼーション プロセスでは、オブジェクトのメモリを完全に開放するために、少なくとも 2 回のコレクション サイクルが必要となります。最初のコレクション パスの間に、オブジェクトはマークされます。ファイナライゼーションは、ガベージ コレクタとは独立した専用のスレッド上で実行されます。ファイナライゼーションの発生後、ガベージ コレクタはオブジェクトのメモリを開放できます。

ファイナライゼーションの非確定性により、オブジェクト コレクションのタイミングや順番について、明確な保証はありません。また、ガベージ コレクションが実行される前に、メモリ リソースが長期間消費されてしまう場合もあります。

C# では、デストラクタの構文によって Finalize を実装してください。

class yourObject {
// ファイナライザを実装
~yourObject() {
// ここでアンマネージ リソースを開放
. . .
}
}

上記の構文をコンパイルすると、以下のコードが生成されます。

class yourObject {
protected override void Finalize() {
try{
. . .
}
finally {
base.Finalize();
}
}

Visual Basic .NET では、Object.Finalize をオーバーライドする必要があります。

Protected Overrides Sub Finalize()
' clean up unmanaged resources
End Sub
Dispose

呼び出しコードによって明示的に開放する必要のある外部リソースへの参照を含む型には、(この章で後述する Dispose パターンを使った) Dispose メソッドを備えてください。IDisposable インターフェイスを実装し、クラスのコンシューマに Dispose の呼び出しを許可することにより、ファイナライゼーションを避けることができます。

ファイナライゼーションを避ける方が好ましいのは、これが非同期で実行される一方、アンマネージ リソースはタイミング良く開放されるとは限らないためです。ビットマップやデータベース コネクションなどの負荷の大きいアンマネージ リソースを扱う場合は、特に注意すべきです。これらの場合、(IDisposable インターフェイスを使って Dispose メソッドを備えることにより) リソースを明示的に開放するという、より一般的なアプローチを使うべきです。これにより、コンシューマが Dispose を呼び出すと、リソースは速やかに回収され、オブジェクトはファイナライゼーションのためにキューに入れられずに済みます。ファイナライザは、バックアップとしてのみ、使うべきです。

このアプローチでは、IDisposable.Dispose メソッドでアンマネージ リソースを開放します。このメソッドは、クラスのコンシューマによって明示的に、または C# の using ステートメントを使って暗黙的に、呼び出すことができます。

ガベージ コレクタがファイナライゼーションを要求しないように、Dispose が GC.SuppressFinalization を呼び出すように実装すべきです。

詳細情報

Dispose メソッドについての詳細は、DataSetSurrogate についての詳細は、Microsoft サポート技術情報の Knowledge Base 文書 315528 「INFO: 派生クラスの実装する Dispose メソッド」を参照してください。

Close

ファイルやデータベース コネクションオブジェクトなどの一定クラスのオブジェクトについては、コンシューマがオブジェクトを使い終わったときに実行すべき論理処理として、Close メソッドが推奨されます。その結果、多くのオブジェクトは、Dispose メソッドに加えてClose メソッドを提供しています。適切に書かれたコードでは、この 2 つのメソッドは同等に機能します。

Dispose パターン

Dispose パターンは、呼び出し元が明示的に開放することを認められるリソースを保持する、すべてのマネージ クラスにおける破棄 (ファイナライザ)機能の実装方法を定義するものです。Dispose パターンを実装するためには、以下を行ってください。

  • IDisposable から派生クラスを作る。
  • IDisposable.Dispose が既に呼び出されたかをトラックするため、プライベート メンバ変数を加える。クライアントが、例外を発生させることなくメソッドを複数回呼び出すことを、許可すべきです。Dispose の呼び出し後にクラスの他のメソッドが呼び出された場合は、ObjectDisposedException をスローすべきです。
  • 1 つのブール値を引数に取る Dispose メソッドの protected virtual void をオーバーライドする。このメソッドには、クライアントが明示的に IDisposable.Dispose を呼び出したとき、またはファイナライザ動作時に呼び出す、通常のクリーンアップ コードが含まれます。ブール値の引数は、クリーンアップの実行が、クライアントによる IDisposable.Dispose 呼び出しによるものなのか、ファイナライゼーションによるものなのかを示すために使用されます。
  • 引数を取らない IDisposable.Dispose メソッドを実装する。このメソッドは、クラアントがリソースの開放を明示的に強制実行するために呼び出します。Dispose が以前に呼び出されているかを確認してください。呼び出されていなければ、 Dispose(true) を呼び出し、続いて GC.SuppressFinalize(this) を呼び出してファイナライゼーションを防ぎます。クライアントがリソースの開放を明示的に実行しているため、ファイナライゼーションはもう必要ありません。
  • デストラクタの構文を使ってファイナライザを作成する。作ったファイナライザで、Dispose(false) を呼び出します。

C# での Dispose 使用例

以下のようなコードになります。

public sealed class MyClass: IDisposable
{
// Dispose が呼び出されたかを調べる変数
private bool disposed = false;
// IDisposable.Dispose() メソッドを実装する
public void Dispose(){
// Dispose が既に呼び出されたかを確認する
if (!disposed)
{
// 通常のクリーンアップ コードを含むオーバーライドした Dispose メソッドを呼び出す
// Dispose によって呼び出されたことを示すために true を渡す
Dispose(true);
//このオブジェクトに対してファイナライゼーションが実行されないようにする
// マネージ / アンマネージ リソースは明示的に開放されているためファイナライゼーションは不要
GC.SuppressFinalize(this);
}
}
// デストラクタの構文を使ってファイナライザを実装する
~MyClass() {
// 通常のクリーンアップ コードを含むオーバーライドした Dispose メソッドを呼び出す
// Dispose によって呼び出されたものでないことを示すために false を渡す
Dispose(false);
}
// 通常のクリーンアップ機能を含む Dispose メソッドを
// オーバーライドする
protected virtual void Dispose(bool disposing){
if(disposing){
// Dispose のコード
. . .
}
// Finalize のコード    . . .
}
...
}

protected Dispose メソッドに true を渡すと、破棄専用のコードが呼び出されます。false を渡すと、破棄専用のコードはスキップされます。Dispose(bool) メソッドは、クラスから直接的に呼び出すことも、クライアントから間接的に呼び出すこともできます。

Finalize / Dispose のコードで静的変数または静的メソッドを参照している場合、Environment.HasShutdownStarted プロパティをチェックしてください。オブジェクトがスレッド セーフなら、クリーンアップに必要なすべてのロックを実施してください。

オブジェクトの Dispose メソッドで HasShutdownStarted プロパティを使用し、CLR がシャット ダウンしているのか、アプリケーション ドメインがアンロードしているのかを判定してください。CLR がシャット ダウンしているなら、ファイナライゼーション メソッドを持ち、静的フィールドによって参照されているオブジェクトには、安定的にアクセスできません。

protected virtual void Dispose(bool disposing){
if(disposing){
// Dispose のコード
. . .
}
// Finalize のコード
CloseHandle();
if(!Environment.HasShutDownStarted)
{ // Debug.Write または Trace.Write - 静的メソッド
Debug.WriteLine("Finalizer Called");
}
disposed = true;
}

Visual Basic .NET での Dispose 使用例

Visual Basic .NET での Dispose パターンの使用例を、以下のコード サンプルで示します。

'Visual Basic .NET Code snippet
Public Class MyDispose Implements IDisposable
Public Overloads Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me) ' No need call finalizer
End Sub
Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
If disposing Then
' Free managed resources
End If
' Free unmanaged resources
End Sub
Protected Overrides Sub Finalize()
Dispose(False)
End Sub
End Class

Finalize と Dispose に関するガイドライン

ここでは、Finalize と Dispose の使用についての推奨事項をまとめます。

  • クラスがサポートしている場合は Close か Dispose を呼び出す。
  • C# の using ステートメントや Visual Basic .NET の Try/Finally ブロックを使用して Dispose が呼び出されるようにする。
  • 必要でない限り Finalize を実装しない。
  • クライアントの呼び出しをまたいでアンマネージ リソースを保持する場合のみ Finalize を実装する。
  • ファイナライゼーションの負荷をオブジェクト グラフのリーフへ移す。
  • Finalize を実装するなら IDisposable も実装する。
  • Finalize と Dispose を実装するなら Dispose パターンを使う。
  • Dispose メソッドでファイナライゼーションを防ぐ。
  • Dispose の複数回呼び出しを許可する。
  • 基底クラスや IDisposable メンバで Dispose を呼び出す。
  • ファイナライザ コードを単純にしてブロックを防ぐ。
  • 型がスレッド セーフな場合のみスレッド セーフなクリーンアップ コードを提供する。

クラスがサポートしている場合は Close か Dispose を呼び出す

使用しているマネージ クラスが Close か Dispose を実装しているなら、オブジェクトの使用が終わり次第、どちらかを呼び出してください。リソースが適用範囲外となるまで、放置しないようにしてください。オブジェクトは、できる限り速やかに開放したい、負荷の大きいネイティブな共有リソースを保持している場合に、Close や Dispose を実装します。

破棄可能なリソース

よく使われる破棄可能な共有リソースには、以下などがあります。

  • データベース関連のクラス: SqlConnection、SqlDataReader、SqlTransaction
  • ファイルに基づくクラス: FileStream、BinaryWriter
  • ストリームに基づくクラス: StreamReader、TextReader、TextWriter、BinaryReader、TextWriter
  • ネットワークに基づくクラス: Socket、UdpClient、TcpClient

.NET Framework における IDisposable を実装するすべてのクラスについては、MSDN 上の .NET Framework クラス ライブラリの「IDisposable インターフェイス」 を参照してください。

COM オブジェクト

COM オブジェクトを要求ごとに作成 / 破棄するサーバー シナリオでは、System.Runtime.InteropServices.Marshal.ReleaseComObject の呼び出しが必要となる場合があります。

ランタイム呼び出し可能ラッパー (RCW) は、COM インターフェイス ポインタがマップされるたびにインクリメントする参照カウント (IUnknown AddRef/Release メソッドの参照カウントとは異なる) を備えています。ReleaseComObject メソッドは、RCWのカウントをデクリメントします。参照カウントが 0 になると、ランタイムはアンマネージ COM オブジェクト上のすべての参照を開放します。

例えば、ASP.NET ページから COM オブジェクトを作成 / 破棄し、そのライフ タイムを明示的にトラックできる場合は、ReleaseComObject の呼び出しを試み、スループットの向上を確認すべきです。

RCW と ReleaseComObject についての詳細は、第 7 章「相互運用パフォーマンスの向上」を参照してください。

Enterprise Services (COM+)

オブジェクトが非デフォルト コンテキストで作られている場合は、サービス コンポーネントや COM / COM+ オブジェクトを共有すべきではありません。コンポーネントが、COM + で構成されたサービス コンポーネントである場合や、クライアントによって非デフォルト コンテキストに置かれたシンプルな COM コンポーネントである場合、オブジェクトは非デフォルト コンテキストとなります。例えば、トランザクションや ASPCOMPAT モードで動作する ASP.NET ページなどのクライアントは、常に COM+ コンテキスト内に置かれます。クライアントがサービス コンポーネント自体である場合も、同様です。

サービス コンポーネントを共有しないのは、主として、COM+ テキスト境界を越えると、負担が大きくなるためです。クライアント側の COM+ コンテキストがスレッド アフィニティを持つ場合は、STA 内に配置されるため、特に負担が大きくなります。

このような場合は、取得、作業、開放の原則に従うべきです。コンポーネントを動作させ、それを用いて作業を実行し、終わったら速やかに開放してください。Enterprise Services と System.EnterpriseServices.ServicedComponent の派生クラスを使うときは、それらのクラスで Dispose を呼び出す必要があります。

呼び出すコンポーネントがアンマネージ COM+ コンポーネントなら、Marshal.ReleaseComObject を呼び出す必要があります。未設定 COM コンポーネント (COM+ カタログにインストールされていないコンポーネント) の場合、クライアントが COM+ コンテキスト内にあり、COM コンポーネントが軽快に動作するものでなければ、やはり Marshal.ReleaseComObject を呼び出すべきです。

サービス コンポーネントの適切なクリーンアップについての詳細は、第 8 章「Enterprise Services パフォーマンスの向上」の「リソース管理」を参照してください。

C# の using ステートメントや Visual Basic .NET の Try/Finally ブロックを使用して Dispose が呼び出されるようにする

例外発生時でもメソッドが呼び出されるよう、Visual Basic .NET コードの Finally ブロック内で Close か Dispose を呼び出してください。

Dim myFile As StreamReader
myFile = New StreamReader("C:\\ReadMe.Txt")
Try
String contents = myFile.ReadToEnd()
'... use the contents of the file
Finally
myFile.Close()
End Try

C# の using ステートメント

開発者が C# を使用している場合、using ステートメントにより、using ブロック内に割り当てられたオブジェクトで Dispose を呼び出す try/finally ブロックを、コンパイル時に自動生成できます。以下は、そのコード例です。

using( StreamReader myFile = new StreamReader("C:\\ReadMe.Txt")){
string contents = myFile.ReadToEnd();
//... ファイルの中身を使う
} // Disposeを呼び出し、StreamReader のリソースを開放する
以下は、上記コードを同等のコードに変換したものです。
StreamReader myFile = new StreamReader("C:\\ReadMe.Txt");
try{
string contents = myFile.ReadToEnd();
//... ファイルの中身を使う
}
finally{
myFile.Dispose();
}

メモ: Visual Basic .NET の次回改訂版では、using ステートメントと同等の機能が導入される予定です。

必要でない限り Finalize を実装しない

ファイナライザを必要としていないクラスでファイナライザを実装すると、ファイナライザ スレッドとガベージ コレクタへの負荷が増加します。ファイナライゼーションが必要でない限り、ファイナライザやデストラクタは実装しないようにしてください。

ファイナライザを持つクラスは、少なくとも 2 サイクルのガベージ コレクションを要求します。これにより、メモリ使用時間が長期化し、メモリ使用量の増加につながる場合もあります。ガベージ コレクタは、ファイナライゼーションを要求する未使用のオブジェクトを見つけると、"ファイナライゼーション準備済み" リストへ移します。オブジェクトのメモリのクリーンアップは、1 つの専用ファイナライザ スレッドが、オブジェクトに登録されたファイナライザ メソッドを実行可能になるまで、延期されます。ファイナライザ動作後、オブジェクトはキューから取り除かれ、文字通り 2 度目の死を迎えます。この時点で、オブジェクトは他のオブジェクトと共に回収されます。クラスがファイナライゼーションを要求していない場合は、Finalize メソッドを実装しないでください。

クライアントの呼び出しをまたいでアンマネージ リソースを保持する場合のみ Finalize を実装する

クライアントの呼び出しをまたいでアンマネージ リソースを保持するオブジェクトのみで、ファイナライザを使ってください。例えば、オブジェクトが GetData という名の接続を開き、アンマネージ リソースからデータを取得し、接続を閉じ、データを返すメソッドを 1 つだけ持っている場合は、ファイナライザを実装する必要はありません。しかし、オブジェクトが Open メソッドによってアンマネージ リソースへの接続を作り、その後で別の GetData メソッドを使ってデータを取得している場合は、呼び出しをまたいでアンマネージ リソースへの接続を保つことは可能です。この場合は、Finalize メソッドを備えることにより、アンマネージ リソースへの接続をクリーンアップする必要があります。また、Dispose パターンを使い、クライアントがリソースを使用後に明示的に開放できるようにするべきです。

メモ: アンマネージ リソースは、直接保持しなければなりません。マネージ ラッパーを使う場合は、自分でファイナライザを持つ必要はありません。ただし、IDisposable の実装を選択し、破棄要求をベースとなるオブジェクトへ渡すことは可能です。

ファイナライゼーションの負荷をオブジェクト グラフのリーフへ移す

オブジェクト グラフで、オブジェクトが、アンマネージ リソースを保持する他のオブジェクト (リーフ) を参照している場合、ファイナライザはルーフ オブジェクトでなく、リーフ オブジェクトに実装すべきです。

これには、いくつかの理由があります。まず、ファイナライズされるオブジェクトは、最初のコレクションで回収されず、ファイナライゼーション リストに入れられます。オブジェクトが回収されなかったということは、他のオブジェクトと同じように、上のジェネレーションへ移されることが考えられ、将来的な回収コストは増加し得るということになります。次に、オブジェクトが回収されなかったため、それが保持しているオブジェクトや、さらにそれらが保持しているオブジェクトなども、回収されません。従って、オブジェクト グラフにおけるファイナライズされたオブジェクトより下の部分全体が、必要以上に長く存続し、より負担のかかるジェネレーションで回収されることになります。

ファイナライズ可能なオブジェクトは常に、オブジェクト グラフのリーフのオブジェクトとなるようにし、これらの問題が起きないようにしてください。それらのオブジェクトは、ラップするアンマネージ リソース以外には、何も保持しないようにすべきです。

ファイナライゼーションの負荷をリーフ オブジェクトへ移すと、ファイナライゼーション キューに関連するオブジェクトのみが昇格することになります。この結果、ファイナライゼーション プロセスを最適化することができます。

Finalize を実装するなら IDisposable も実装する

ファイナライザを実装するなら、IDisposable も実装すべきです。これにより、呼び出しコードは Dispose メソッドを呼び出してリソースを明示的に開放できるようになります。

呼び出しコードが常に Dispose を呼び出すとは限らないため、Dispose と共にファイナライザも実装する必要があります。コストはかかりますが、ファイナライザの実装により、リソースの開放を保証できます。

Finalize と Dispose を実装するなら Dispose パターンを使う

Finalize と Dispose を実装するなら、前述したように Dispose パターンを使ってください。

Dispose メソッドでファイナライゼーションを防ぐ

Dispose メソッドを提供する目的は、呼び出しコードによってアンマネージ リソースをできる限り速やかに開放できるようにすることと、オブジェクトのクリーンアップに 2 サイクルを費やすのを防ぐことにあります。呼び出しコードが Dispose を呼び出す場合、アンマネージ リソースはオペレーティング システムへ戻ることになるため、ガベージ コレクタがファイナライザを呼び出すことは望ましくないでしょう。そのため、Dispose メソッドの GC.SuppressFinalization を使い、ガベージ コレクタがファイナライザを呼び出すのを防ぐべきです。

public void Dispose()
{
// Dispose パターンを使う
Dispose(true);
// ... ここでアンマネージ リソースを開放
GC.SuppressFinalize(this);
}

Dispose の複数回呼び出しを許可する

呼び出しコードが、例外を発生させることなく、Dispose を複数回呼び出せるようにするべきです。最初の呼び出しの後、以後の呼び出しは何もするべきではなく、以後の呼び出しのために ObjectDisposedException をスローすべきでもありません。

ObjectDisposedException は、Dispose の呼び出し後に、呼び出し元のクラスの (Dispose を除く) 他のメソッドからスローするべきです。

Dispose が呼び出されたかを示すプライベート変数を保持するのが、一般的なアプローチです。

public class Customer : IDisposable{
private bool disposed = false;
. . .
public void SomeMethod(){
if(disposed){
throw new ObjectDisposedException(this.ToString());
}
. . .
}
public void Dispose(){
// Dispose パターンの呼び出し前にチェック
if (!disposed)
{ ... }
}
. . .
}

基底クラスや IDisposable メンバで Dispose を呼び出す

クラスが、破棄可能なクラスから継承したものである場合、基底クラスの Dispose を呼び出すようにしてください。また、IDisposable を実装しているメンバ変数がある場合も、それらで Dispose を呼び出してください。

基底クラスで Dispose を呼び出す場合のコード例を、以下で示します。

public class BusinessBase : IDisposable{
public void Dispose() {...}
protected virtual void Dispose(bool disposing)  {}
~BusinessBase() {...}
}

public class Customer : BusinessBase, IDisposable{
private bool disposed = false;

protected virtual void Dispose(bool disposing) {
// Check before calling your Dispose pattern
if (!disposed){
if (disposing) {
// free managed objects
}
// free unmanaged objects
base.Dispose(disposing);
disposed = true;
}
}

ファイナライザ コードを単純にしてブロックを防ぐ

ファイナライザ コードは、単純かつ最小限とするべきです。ファイナライゼーションは、1 つの専用ファイナライザ スレッド上で発生します。ファイナライザ コードには、以下のガイドラインを適用してください。

  • 呼び出しスレッドをブロックし得る呼び出しを発行しない。ファイナライザがブロックされると、リソースは開放されず、アプリケーションでメモリ リークが発生します。
  • スレッド ローカル ストレージなどのスレッド アフィニティを必要とするアプローチを使わない。ファイナライザ メソッドは、アプリケーションのメイン スレッドから分離した、専用スレッドから呼び出されるためです。

複数のスレッドがファイナライゼーション可能なオブジェクトを多数割り当てている場合は、ファイナライザ スレッドが一定時間内にクリーンアップ可能な数よりも多いファイナライザ可能なオブジェクトを、同じ時間内に割り当ててしまう場合も考えられます。このため、Microsoft は将来的に、CLR で複数のファイナライザ スレッドを実装することを選択するかもしれません。従って、共有状態に依存しないファイナライザを作成すべきです。依存する場合はロックを使い、同じファイナライザ メソッドの他のインスタンスによる、異なるオブジェクト インスタンスへの同時アクセスを防ぐべきです。しかし、それ以前にファイナライザ コードを単純なものにするように務め (例えば、CloseHandle 呼び出し以上に難しいことはしない)、これらの問題を避けるべきです。

型がスレッド セーフな場合のみスレッド セーフなクリーンアップ コードを提供する

型がスレッド セーフなら、クリーンアップ コードもスレッド セーフにしてください。例えば、スレッド セーフな型がリソースのクリーンアップ用に CloseDispose の両方のメソッドを備えているなら、CloseDispose を呼び出すスレッドを、同期するようにしてください。

ピニング(固定化)

アンマネージ サービスと安全に通信するためには、ガベージ コレクタにメモリ内の一定のオブジェクトを再配置させないようにすることが必要となる場合もあります。このようなオブジェクトを、"固定された" オブジェクトと呼び、このプロセスを "ピニング" と呼びます。ガベージ コレクタは固定されたオブジェクトを移動できないため、マネージ ヒープは普通のヒープのように断片化されることもあり、この場合は使用可能なメモリが少なくなります。ピニングは、明示的にも暗黙的にも実行可能です。

  • 暗黙的なピニングは、P/Invoke 状況や COM 相互運用状況のほとんどで、文字列などの引数を渡す場合に実行されます。
  • 明示的なピニングは、さまざまな方法で実行可能です。GCHandle を作成し、引数として GCHandleType.Pinned を渡すことができます。
    GCHandle hmem = GCHandle.Alloc((Object) someObj, GCHandleType.Pinned);
  • コードのアンセーフ ブロックで fixed ステートメントを使うこともできます。
    // Circle クラスを作っておく { public int rad; }
    Circle cr = new Circle; // cr はマネージ変数で gc を受ける.
    fixed ( int* p = &cr.rad ){ // fixed を使い cr.rad のアドレスを取得
    *p = 1; // ポインタを使いながら cr をピニング
    }
    

バッファをピニングする場合は起動時に割り当てる

遅い I/O 処理の前にバッファを割り当て、それらをピニングすると、ヒープの断片化によってメモリが過度に消費されかねません。割り当てられたばかりのメモリは、ほとんどが Gen 0、そうでなくても Gen 1 であるため、ピニングすべきではありません。これらのジェネレーションは設計上、最も頻繁に圧縮されます。固定されたオブジェクトのそれぞれは、非常に負担が大きく、断片化につながる圧縮プロセスを行うことになってしまいます。最も下のジェネレーションで、これらのコストを費やすべきではありません。

このような問題を避けるには、これらのバッファをアプリケーションの起動時に割り当て、すべての I/O 処理のためのバッファ プールとして扱うべきです。オブジェクトを早く割り当てるほど、早く Gen 2 へ到達可能になります。オブジェクトが Gen 2 に達した後は、圧縮の頻度が下がるため、ピニングのコストは大幅に減ります。

スレッド処理についての説明

.NET Framework は、スレッド処理や同期のためのさまざまな機能を提供しています。マルチスレッドの使用は、アプリケーションのパフォーマンスやスケーラビリティに大きな影響を及ぼし得ます。

マネージ スレッドとオペレーティング システム スレッド

CLR は、マネージ スレッドを提供します。これは、Microsoft Win32R スレッドとは違うものです。論理スレッドは、マネージで、物理スレッドは、コードを実際に実行する Win32 スレッドです。マネージ スレッドと Win32 スレッドが 1 対 1 の関係で対応しているとは限りません。

マネージ スレッド オブジェクトを作成しても、Start メソッドを呼び出してそれを起動しなければ、新しい Win32 スレッドは作成されません。マネージ スレッドの終了や処理の完了に伴い、ベースとなる Win32 スレッドは破棄されます。マネージ スレッド オブジェクト は、不特定期間後に、ガベージ コレクション中にのみ、破棄されます。

.NET Framework クラス ライブラリは、Win32 スレッドの代表として ProcessThread クラスを、マネージ スレッドの代表として System.Threading.Thread クラスを提供しています。

不適切に作成したマルチスレッド コードは、デッドロック、競合状況、スレッドの枯渇、スレッド アフィニティなど、さまざまな問題を引き起こしかねません。これらの問題はすべて、アプリケーションのパフォーマンス、スケーラビリティ、柔軟性、正確性に悪影響をもたらし得ます。

スレッド処理に関するガイドライン

ここでは、スレッド処理コードの効率性を高めるためのガイドラインをまとめます。:

  • スレッド作成を最小限に抑える。
  • スレッドが必要ならスレッド プールを使う。
  • タイマーを使って定期的タスクを処理する。
  • 並行タスクと同期タスクを対比評価する。
  • 他のスレッドを終了させるために Thread.Abort を使わない。
  • スレッドを中断するために Thread.Suspend と Thread.Resume を使わない。

スレッド作成を最小限に抑える

スレッドはマネージ リソースとアンマネージ リソースの両方を使用し、初期化には大きな負担が伴います。スレッドをむやみに作成すれば、プロセッサでのコンテキスト切り替えの増加につながりかねません。以下のコードでは、新しいコードを作成し、各要求について保持しています。このコードにより、プロセッサは作業のほとんどをスレッド切り替えに費やすことになるかもしれません。また、リソースをクリーンアップしようとするガベージ コレクタに対し、圧力をかけてしまうことにもなります。

private void Page_Load(object sender, System.EventArgs e) 
{
if (Page.IsPostBack)
{                     
// Create and start a thread
ThreadStart ts = new ThreadStart(CallMyFunc);
Thread th = new Thread(ts);
ts.Start();
…….      
}

スレッドが必要ならスレッド プールを使う

CLR スレッド プールを使ってスレッド ベースの作業を実行し、大きな負担をかけるスレッドの初期化を避けてください。以下のコードでは、スレッド プールのスレッドを使用し、メソッドを実行しています。

WaitCallback methodTarget = new WaitCallback( myClass.UpdateCache );
ThreadPool.QueueUserWorkItem( methodTarget );

QueueUserWorkItem が呼び出されると、メソッドは実行待ちのキューに入れられ、処理は呼び出し元のスレッドに戻り、続けられます。ThreadPool クラスは、アプリケーションのプール内のスレッドを、利用可能になると同時に用い、コールバックで渡されたメソッドを実行します。

タイマーを使って定期的タスクを処理する

System.Threading.Timer クラスを使い、定期的タスクを処理してください。Timer クラスにより、コードを実行すべき定期間隔を指定することができます。以下のコードでは、メソッドが 30 秒ごとに呼び出されます。

...
TimerCallback myCallBack = new TimerCallback( myHouseKeepingTask );
Timer myTimer = new System.Threading.Timer( myCallBack, null, 0, 30000);

static void myHouseKeepingTask(object state)
{
...
}

タイマーで設定した間隔ごとに、スレッド プールからスレッドが使われ、TimerCallback の示すコードが実行されます。これにより、新しいスレッドの作成に伴うスレッドの初期化を避けることができるため、最適なパフォーマンスを得られます。

並行タスクと同期タスクを対比評価する

非同期コードを実装する前に、複数のタスクを並行処理することの必要性について、慎重に考慮してください。並行処理の増加は、パフォーマンス メトリクスに大きな影響を及ぼす場合があります。スレッドが増えれば、メモリ、ディスク I/O、ネットワーク帯域、データベース コネクションなどのリソースの消費も増えます。また、スレッドの増加は、競合やコンテキスト切り替えに関するオーバーヘッドを肥大化させる恐れもあります。いずれにしても、スレッドの増加により、パフォーマンス目標値から遠ざかるのか、近付くのかを検証することが大切です。

複数タスクの並行処理が適切となる場合の例を、以下に示します。

  • あるタスクが他のタスクの結果に依存しておらず、他を待つことなく処理を進めることができる。
  • 作業が I/O 依存の場合。I/O に関連するタスクでは、I/O 処理中はスレッドが休止し、他のスレッドを実行できるため、タスク自身にスレッドを持たせるとメリットを得られます。一方、CPU 依存の作業では、並行処理はパフォーマンスにマイナスの影響を及ぼしがちです。

他のスレッドを終了させるために Thread.Abort を使わない

他のスレッドを終了させるために Thread.Abort を使わないようにしてください。Abort を呼び出すと、CLR は ThreadAbortException をスローします。Abort の呼び出しは、スレッド終了に直接つながっているわけではなく、終わらせようとするスレッドで例外を発生させます。Thread.Join を使えば、スレッドの終了を確かめるまで待機することができます。

スレッドを中断するために Thread.Suspend と Thread.Resume を使わない

複数スレッドの動作を同期させるために Thread.Suspend と Thread.Resume を呼び出さないでください。優先度の低いスレッドを中断するために Suspend を呼び出してはなりません。スレッドを強制的に操作するよりも、Thread.Priority プロパティを設定することを検討してください。

あるスレッド上で他のスレッドから Suspend を呼び出すのは、極めて強制的な処理で、深刻なアプリケーションのデッドロックにつながりかねません。例えば、中断しようとするスレッドが、他のスレッドや Suspend を呼び出したスレッドが必要とするリソースを保持しているかもしれません。

複数のスレッドの動作を同期させる必要がある場合は、lock(object)、Mutex、ManualResetEvent、AutoResetEvent、Monitor の各オブジェクトを使用してください。これらは、すべて WaitHandle クラスの派生オブジェクトで、プロセス内やプロセス間でスレッドを同期させるために使うことができます。

メモ: lock(object) は最も負担の小さい処理で、同期が必要となる場合のすべてではないにしても、ほとんどに対応します。

詳細情報

詳細は、以下の資料を参照してください。:

非同期呼び出しについての説明

非同期呼び出しにより、アプリケーションの並行処理能力を高めることができます。非同期呼び出しはブロックを伴わない呼び出しで、メソッドを非同期で呼び出すと、処理は速やかに呼び出し元のスレッドに戻り、現在のメソッドの実行を続けます。

メソッドを非同期呼び出しする方法には、いくつかあります。

  • 非同期コンポーネントを呼び出す - クラスの中には、BeginInvoke メソッドと EndInvoke メソッドを提供することにより、.NET Framework 非同期呼び出しモデルをサポートしているものもあります。クラスが、1 単位の作業の終了時に EndInvoke を明示的に呼び出すことを求めている場合は、その通りにしてください。これにより、非同期呼び出しに障害があれば、それを検知することも可能になります。
  • 非同期でないコンポーネントを呼び出す - クラスが BeginInvoke メソッドと EndInvoke メソッドをサポートしていない場合は、以下のいずれかのアプローチを用いることができます。
    • .NET スレッド プールを使う。
    • スレッドを明示的に作成する。
    • デリゲートを使う。
    • タイマーを使う。

非同期に関するガイドライン

ここでは、非同期実行を検討している状況での、パフォーマンスを最適化するためのガイドラインを提示します。

  • UI 応答性を考慮してクライアント側での非同期呼び出しを検討する。
  • I/O 依存の処理についてサーバーで非同期メソッドを使用する。
  • 並行処理性の向上につながらない非同期呼び出しを避ける。

UI 応答性を考慮してクライアント側での非同期呼び出しを検討する

非同期呼び出しを用いて、クライアント アプリケーションの応答性を高めることができます。ただし、非同期呼び出しはプログラミングを複雑にし、グラフィック インターフェイス コードでは注意を要する同期ロジックが必要となるため、採用する前に慎重に検討してください。

以下のコードでは、非同期呼び出しを発行し、これが完了するまでループしています。呼び出し完了前に関数から抜け出す必要がある場合は、while の条件部分に終了基準を追記可能です。クライアントを更新する必要が無い場合は、コールバックを使うか、完了するまで待機することができます。

IAsyncResult CallResult = SlowCall.BeginInvoke(slow,null,null);
while ( CallResult.IsCompleted == false)
{
... // ユーザー フィードバックを供給
}
SlowCall.EndInvoke(CallResult);

I/O 依存の処理についてサーバーで非同期メソッドを使用する

複数の処理を同時に実行することにより、アプリケーションのパフォーマンスを向上させることができます。2 つの処理は、互いに依存していません。例えば、以下のコードでは、2 つの Web サービスを呼び出しています。コードの実行に要する時間は、2 つのメソッドの処理時間の和となります。

// プロキシに参照を取得
EmployeeService employeeProxy = new EmployeeService();
// 1 つ目のメソッドを実行し、完了するまでブロック
employeeProxy.CalculateFederalTaxes(employee, null, null);
// 2 つ目のメソッドを実行し、完了するまでブロック
employeeProxy.CalculateStateTaxes(employee);

コードを以下のようにリファクタリングし、処理の総存続時間を短縮することができます。以下のコードでは、2 つのメソッドが同時に処理され、処理の存続時間は短くなります。下の例で、BeginCalculateFederalTaxes メソッドを使っていることに注意してください。これは、非同期タイプの CalculateFederalTaxes メソッドです。これらのメソッドは、Visual Studio .NET でクライアント アプリケーションから Web サービスを参照したときに、自動的に生成されます。

// 参照をプロキシに取得
EmployeeService employeeProxy = new EmployeeService();
// 非同期呼び出し BeginCalculateFederalTaxes を開始
// 呼び出し元へ速やかに戻り、ローカル処理の続行可能
IAsyncResult ar = employeeProxy.BeginCalculateFederalTaxes(employee, null, null);
// CalculateStateTaxes を同期実行
employeeProxy.CalculateStateTaxes(employee);
// CalculateFederalTaxes 呼び出しが完了するまで待機
employeeProxy.EndCalculateFederalTaxes(ar);

詳細情報

詳細は、第 10 章「Web サービス パフォーマンスの改善」の「非同期 Web メソッド」を参照してください。

並行処理性の向上につながらない非同期呼び出しを避ける

同一処理で複数のスレッドをブロックする同期呼び出しを避けてください。以下のコードでは、Web サービスを非同期で呼び出しています。呼び出し元のコードは、Web サービス呼び出しが完了するのを待っている間、ブロック状態となります。非同期呼び出しが実行されている間、呼び出し元のコードは何もしていないことに注意してください。

// プロキシを Web サービスに取得
customerService serviceProxy = new customerService;
// CustomerUpdate の非同期呼び出しを開始
IAsyncResult result = serviceProxy.BeginCustomerUpdate(null,null);
// ここで別の作業を並行処理するべきだが
// 何もせず
// 非同期処理の完了待ち
// 呼び出し完了までクライアントをブロック
result.AsyncWaitHandle.WaitOne();
serviceProxy.EndCustomerUpdate(result);

このようなコードが ASP.NET アプリケーションなどのサーバー アプリケーションや Web サービスで実行されると、1 つのタスクの処理のために 2 つのスレッドを実行しながら、何のメリットも得られないことになります。それだけでなく、他の要求の処理を遅れさせてしまいます。従って、このようなアプローチは避けるべきです。

ロックと同期についての説明

ロックと同期により、並行処理を避けるための、データやコードへの排他的なアクセスが可能になります。

この節では、ロックと同期について、正しいアプローチをとるために考慮すべきステップをまとめます。

  • 同期の必要性を判断する。
  • アプローチを決定する。
  • アプローチの範囲を決める。

同期の必要性を判断する。

同期について考慮する前に、粗結合など、同期を用いなくて済むような、他のアプローチを検討すべきです。同期が特に必要となるのは、制的データなどの共有リソースが、複数のユーザーによって同時にアクセス、または更新される場合です。

アプローチを決定する

CLR は、ロックと同期のために、以下のメカニズムを提供しています。状況に合わせて、適切なものを選択してください。

  • lock (C#) - C# コンパイラは、lock ステートメントから try/finally ブロックを生成し、 Monitor.Enter および Monitor.Exit 呼び出しを実行します。Visual Basic .NET では、SyncLock を使用してください。
  • WaitHandle クラス - このクラスは、複数オブジェクトへの同時排他アクセスを待つための機能を提供します。WaitHandle には、3 つの派生クラスがあります。
  • ManualResetEvent - コードは、手動リセットされる信号を待ちます。
  • AutoResetEvent - コードは、自動リセットされる信号を待ちます。
  • Mutex - WaitHandle の特別タイプで、プロセスをまたいでの使用をサポートします。Mutex オブジェクトには、参照が不要となるように、固有の名前を付けることができます。異なるプロセスに存在するコードは、名前によって同じ Mutex にアクセス可能です。
  • MethodImplOptions.Synchronized 列挙オプション - これにより、メソッド全体への排他アクセスを許可できるようになります。このアプローチが望ましいケースは稀です。
  • Interlocked クラス - 型のためにアトミック インクリメント / デクリメント メソッドを提供します。Interlocked は、値型で使用可能です。また、比較に基づいて値を置き換える機能もサポートしています。
  • Monitor オブジェクト - 参照型への同期アクセスのための静的メソッドを提供します。また、オーバーライドしたメソッドを提供し、コードが指定期間のロックを試みることを可能にします。Monitor クラスは、値型と共には使用できません。値型は、Monitor と共に使用されるときはボックス化され、ロックしようとするたびに、ボックス化された他とは異なるオブジェクトが新しく生成されます。これにより、排他アクセスは無効となります。C# では、値型について Monitor を使用すると、エラー メッセージを示します。

アプローチの範囲を決める

粒度レベルに応じて、型から個々のメソッド内のコードの特定行まで、異種のオブジェクトをロックすることができます。どのようなロックを持っていて、それをどこで取得し、どこで開放するかを把握してください。同期メカニズムを提供するため、以下について一貫してロックするポリシーを採用できます。

  • - 型のロックは避けるべきです (例えば、lock(typeof(type))。型オブジェクトは、アプリケーション ドメインをまたいで共有可能です。型をロックすると、プロセス内のアプリケーション ドメインをまたぐその型のインスタンスは、すべてロックされてしまいます。これは、予期しない結果、特にパフォーマンスの低下を招く場合があります。
  • this - 外部から見えるオブジェクトのロックは避けるべきです (例えば、lock(this))。これは、他のどのコードが、どのような目的やポリシーのために同じロックを取得するか、特定できないためです。正確性を期すため、"this" は避けるのが得策です。
  • クラスのメンバである特定のオブジェクト - 型や型のインスタンス、またはクラス内の "this" よりも、これをロックすべきです。クラスレベルで同期が必要なら、プライベートな静的オブジェクトをロックしてください。関連する各メソッドで、ロックに関するポリシーを一貫して明確に実施してください。

ロック中は、ロックの粒度についても考慮すべきです。各粒度レベルを、以下に掲げます。

  • メソッド -MethodImplOptions.Synchronized 列挙オプションを使用し、インスタンスのメソッド全体への同期アクセスが可能になります。メソッド レベルでのロックは、メソッド内のコードの全行が同期アクセスを必要としているときにのみ、検討すべきです。その他の場合でメソッド レベルでのロックを実装すると、競合の増加につながり得ます。また、共有状態を使用中の他のメソッドに対する保護は、提供されません。従って、メソッドごとに 1 つのロック オブジェクトという形になるため、ポリシーとしてほとんど役立ちません。
  • メソッド内のコード ブロック - ロックするスコープを適切に設定したオブジェクトを選択し、ロックが保護する共有状態を変更するコードに入る直前にロックを取得するようなポリシーを持つことにより、ほとんどの要件を満たすことができます。オブジェクトをロックすることにより、1度にそのコードの 1 部分のみが動作するようにできます。

ロックと同期に関するガイドライン

ここでは、ロックと同期を必要とするマルチスレッド コードの開発時に考慮すべきガイドラインを提供します。

  • ロックを遅く確保し早く手放す
  • 必要でない限りロックと同期を避ける。
  • 過剰な細粒度ロックを避ける。
  • 粒度の小さすぎるロックを避ける。
  • 型をデフォルトでスレッド セーフにしない。
  • Synchronized の代わりに細粒度 lock (C#) ステートメントを使う。
  • this のロックを避ける。
  • ロック代わりに ReaderWriterLock を使用して複数読み出しと単独書き込みを調整する。
  • 同期アクセスのためにオブジェクトの型をロックしない。

ロックを遅く確保し早く手放す

リソースを保持し、ロックする期間を、最小限に抑えてください。ほとんどのリソースは共有され、限られているためです。リソースを早く開放するほど、他のスレッドはそのリソースを早く利用できるようになります。

リソースへのロックはアクセスが必要となる直前に確保し、完了したら速やかに手放してください。

必要でない限りロックと同期を避ける

同期には、リソースへの排他アクセスを許可するための、CLR による特別な処理が必要となります。データへのマルチスレッド アクセスやスレッドの同期が必要でない場合は、同期を実装しないでください。設計や実装に同期をとり入れる前に、以下の選択肢を検討してください。

  • 既存の同期メカニズムを使ったコードを設計する。例えば、ASP.NET アプリケーションが用いる Cache オブジェクト。
  • データの同時変更を避けるコードを設計する。不適切な同期の実装は、アプリケーションの並行処理効果を無効にする場合があります。アプリケーション内で、データの同時変更の可能性をなくすために書き換え可能なコード部分を探してください。
  • 粗結合による並行処理問題への対処を検討する。例えば、ロック競合を最小化するために、イベント デリゲーション モデル (Producer-Consumer (生産者-消費者パターン)) の使用を検討してください。

細粒度ロックによって競合を減らす

ロックは、適切な粒度レベルで正しく使えば、競合を減らすことによって並行処理性の向上に役立ちます。ロックの範囲を決める前に、前述したさまざまな選択肢を検討してください。最も効率的なアプローチは、オブジェクトをロックする際に、その範囲を、コード内の共有リソースにアクセスする部分に限定することです。ただし、デッドロックの可能性には、常に注意してください。

過剰な細粒度ロックを避ける

細粒度ロックは、少量のデータまたは少量のコードを保護します。これを正しく使えば、ロック競合を減らすことにより、並行処理性を向上させることができます。しかし、不適切に使えば、コードは複雑になり、パフォーマンスと並行処理性は低下します。コード内で複数な細粒度ロックを使わないようにしてください。以下のコードは、3 つのリソースを制御するために複数の lock ステートメントを使った例です。

s = new Singleton();
sb1 = new StringBuilder();
sb2 = new StringBuilder();
s.IncDoubleWrite(sb1, sb2);
class Singleton
{
private static Object myLock = new Object();
private int count;
Singleton()
{
count = 0;
}
public void IncDoubleWrite(StringBuilder sb1, StringBuilder sb2)
{
lock (myLock)
{
count++;
sb1.AppendFormat("Foo {0}", count);
sb2.AppendFormat("Bar {0}", count);
}
}
public void DecDoubleWrite(StringBuilder sb1, StringBuilder sb2)
{
lock (myLock)
{
count--;
sb1.AppendFormat("Foo {0}", count);
sb2.AppendFormat("Bar {0}", count);
}
}
}

メモ: すべての例におけるすべてのメソッドは、正確性のためにロックを要求しています (ただし、Interlocked.Increment は他の目的で使うこともできます)。

複数ロックの使用によるリソースの消費を避けるため、ロック可能な最小のコード ブロックを特定してください。

型をデフォルトでスレッド セーフにしない。

型をスレッド セーフにするかを決める際に、以下のガイドラインを考慮してください。

  • インスタンス状態をスレッド セーフにする必要があるとは限らない - デフォルトでは、クラスをスレッド セーフにするべきではありません。シングル スレッド環境や同期環境では、オーバーヘッドの増加につながるためです。ロックを使ってインスタンス状態へのアクセスを同期する必要がある場合もありますが、それはコードが提供するスレッド セーフティ モデルに依存します。例えば、Neutral スレッド処理モデル インスタンスでは、状態情報を保護する必要はありません。一方、フリーなスレッド処理モデルでは、状態情報を保護する必要があります。

    スレッド セーフなコードを作成するためにロックを増加させると、パフォーマンスは低下し、ロック競合は増加します (デッドロック バグの発生率も高まります)。一般的なアプリケーション モデルでは、1 度に 1 つのコードのみがユーザー コードを実行します。これにより、スレッド セーフとする必要性は最小限になります。このため、.NET Framework クラス ライブラリのほとんどは、スレッド セーフではありません。

  • 静的データをスレッド セーフにすることを検討する - 静的データを使わなければならない場合、複数のスレッドや要求による同時アクセスから、それを保護するための方法を検討してください。一般的なサーバーでは、静的データは要求をまたいで共有されます。従って、複数のコードが同時にそのコードを実行できることになります。このため、静的データを同時アクセスから保護することが必要になります。

Synchronized の代わりに細粒度 lock (C#) ステートメントを使う

MethodImplOptions.Synchronized 属性により、どの時点でも、1 つのスレッドのみが属性付きメソッドのどこかで動作しているようにすることができます。ただし、リソースをほとんどロックしない長いメソッドがある場合は、Synchronized でなく lock ステートメントを使うことを検討してください。ロック期間を短縮し、並行処理性を向上させることができます。

[MethodImplAttribute(MethodImplOptions.Synchronized)]
public void MyMethod
// ロックの使用
public void MyMethod()
{
...
lock(mylock)
{
// 個々の部分のコードは mylock を取得した唯一のコードとして想定可能で
// それに応じてリソースを使用可能
...
}
}

this のロックを避ける

正確性を期すため、クラスでの "this" のロックを避けてください。パフォーマンスは向上しません。これについては、以下のアプローチを検討してください。

  • ロックするためのプライベート オブジェクトを作成する。
    public class A {
    ...
    lock(this) { ... }
    ...
    }
    // 以下のコードに変更
    public class A
    {
    private Object thisLock = new Object();
    ...
    lock(thisLock) { ... }
    ...
    }
    

    この結果、同期を必要としないメンバを含め、すべてのメンバがロックされます。

  • 特定のメンバ変数についてアトミックな更新が必要な場合、System.Threading.Interlocked クラスを使用する。

メモ: このアプローチによって正確性の問題を回避できますが、同期を必要としないメンバを含め、すべてのメンバがロックされることになります。従って、粒度を小さくしたロックが最適かもしれません。

ロック代わりに ReaderWriterLock を使用して複数読み出しと単独書き込みを調整する

競合の少ないモニタやロックは、パフォーマンス上、比較的小さな負担で済みますが、競合の多い場合は、負担が大きくなります。ReaderWriterLock は、共有ロック メカニズムを提供するものです。これにより、複数のスレッドがリソースを同時に読み出せるようになりますが、リソースへの排他書き込みを行うスレッドは、待機を求められます。

読み出し / 書き込み時間については、常に最短化を試みるべきです。書き込みロックは排他的であるため、書き込み時間が長いと、アプリケーションのスループットは低下します。また、長い書き込みは、読み出しや書き込みを待つ他のスレッドをブロックする恐れがあります。

詳細は、MSDN ライブラリの 「.NET Framework クラス ライブラリ」の「ReaderWriterLock クラス」 を参照してください。

同期アクセスのためにオブジェクトの型をロックしない

型オブジェクトはアプリケーション ドメインに非依存で、同じインスタンスを複数のアプリケーション ドメインでマーシャリングやクローニングを必要とすることなく使用できます。lock(typeof(type)) を使うオブジェクトの型をロックするポリシーを実装している場合は、プロセス内のアプリケーション ドメインをまたいで、オブジェクトのすべてのインスタンスをロックすることになります。

型全体をロックしてしまう例を、以下で示します。

lock(typeof(MyClass))
{
// 独自コード
}

代わりに、その型の静的オブジェクトを作成してください。このオブジェクトをロックし、同期アクセスを提供することができます。

class MyClass{
private static Object obj = new Object();
public void SomeFunc()
{
lock(obj)
{
// 何らかの処理を実行
}
}
}

メモ: 1つの lock ステートメントが、他のコードによる保護リソースへのアクセスを妨げることはありません。実際に保護されるのは、一定の処理の前にあるロックを常に確保するというポリシーが実装されている場合のみです。

同じ理由により、文字列、アセンブリ インスタンス、バイト配列など、アプリケーション ドメインに非依存な他の型についても、ロックを避けるべきです。

値型と参照型

.NET Framework におけるデータ型は、すべて値型か参照型に分けることができます。この節では、これらの 2 つの基本型について説明します。表 5.2 は、値型と参照型のそれぞれに分類される代表的なデータ型を示したものです。

表 5.2: 値型と参照型

値型参照型
列挙型クラス
構造体デリゲート
BooleanDateChar などのプリミティブ型例外
Decimal などの数値型属性
ByteShortIntegerLong などの整数型配列
SingleDouble などの浮動小数点型

値型を格納するためのメモリは、現在のスレッドのスタック領域に割り当てられます。値型のデータは、完全にこのメモリ アロケーション内で保持されます。値型を格納するためのメモリは、それが作成されたスタックが存続している間のみ、保持されます。値型のデータは、メソッドの引数として渡されるか、値型を参照型に割り当てることによってコピーが作られると、スタック フレーム外で存続可能となります。値型は、デフォルトでは値渡しされます。値型が参照型の引数に渡されると、ラッパー オブジェクトが作成され (値型がボックス化され)、値型のデータはラッパー オブジェクト内にコピーされます。例えば、オブジェクトを受けるメソッドに整数を渡すと、ラッパー オブジェクトが作成されます。

参照型

値型と対照的に、参照型オブジェクトのデータは、常にマネージ ヒープに格納されます。参照型の変数は、そのデータへのポインタのみで構成されます。クラス、デリゲート、例外などの参照型を格納するためのメモリは、参照されなくなった時点でガベージ コレクタによって回収されます。参照型は、常に参照渡しされます。これは、知っておくべき鉄則です。参照型を値渡しするように指定する場合は、その参照のコピーが作成され、そのコピーへの参照が渡されます。

ボックス化とボックス化解除についての説明

値型を参照型に変換することや、それを戻すことができます。値型の変数を参照 (オブジェクト) 型の変数に変換する必要がある場合は、値を保持するためのオブジェクト (ボックス) がマネージ ヒープに割り当てられ、値がボックス内にコピーされます。このプロセスは、ボックス化 と呼ばれています。ボックス化は、以下のコードで示すように、暗黙的にも明示的にも行えます。

int p = 123;
Object box;
box = p; // 暗黙的なボックス化
box = (Object)p; // キャストを伴う明示的なボックス化

ボックス化は、オブジェクトを引数として受けるメソッドに値型を渡す場合に、最もよく使われます。オブジェクト内の値を値型に戻す場合は、値がボックスから適切な格納場所へコピーされます。このプロセスは、ボックス化解除と呼ばれています。

p = (int)box; // ボックス化解除

ボックス化に関する問題は、ループ時や、値型を格納する大型のコレクションなどの大量データを扱う場合に複雑になります。

ボックス化とボックス化解除に関するガイドライン

ボックス化やボックス化解除がパフォーマンスに深刻な悪影響をもたらさないように、以下の推奨事項に従ってください。

  • 頻繁なボックス化やボックス化解除によるオーバーヘッドを避ける。
  • ボックス化オーバーヘッドを計測する。.
  • Visual Basic .NET コードでは DirectCast を使う。

頻繁なボックス化やボックス化解除によるオーバーヘッドを避ける

ボックス化するためには、ヒープ アロケーションやメモリのコピーが必要となります。ボックス化を避けるために、値型を参照型として扱わないようにしてください。参照型の引数をとるメソッドに、値型を渡さないでください。ボックス化が避けられない場合は、ボックス化 オーバーヘッドを抑えるために、変数を 1 度ボックス化し、ポックス化されたコピーへのオブジェクト参照を必要な間保持し続け、再び値型が必要になったらボックス化解除するようにしてください。

int p = 123;
object box;
box = (object)p; // キャストを伴う明示的ボックス化
// p の代わりにボックス変数を使用

メモ: Visual Basic .NET では、C# よりもボックス化が頻繁に発生しやすくなります。これは、値渡しを基本とする言語特性と、GetObjectValue を呼び出す必要性があるためです。

コレクションとボックス化

コレクションは、オブジェクトを基本型とするデータのみを格納します。整数型や浮動小数点型などの値型をコレクションに渡すと、ボックス化が発生します。よくあるのは、データベースから返された int 型や float 型を含むデータをコレクションに格納するというパターンです。コレクションでは、反復処理のためにオーバーヘッドが極端に大きくなってしまう場合があります。以下のコードは、この問題を例示したものです。

ArrayList al = new ArrayList();
for (int i=0; i<1000;i++)
al.Add(i); // Add() はオブジェクトをとるため、暗黙的にボックス化
int f = (int)al[0]; // 要素をボックス化

これを防ぐためには、代わりに配列を使うか、特定の値型のための独自コレクション クラスを作成することを検討してください。ボックス化解除は、明示的なキャスト演算子と共に実行しなければなりません。

メモ: 本稿執筆時点では、.NET Framework 2.0 は C# 言語に ジェネリクを導入しています。これにより、上記のコードを、ボックス化を伴わないものに書き直すことができます。

ボックス化オーバーヘッドを計測する

ボックス化による影響を計測する方法は、いくつかあります。まず、パフォーマンス モニタを使って、アプリケーションのリソース利用度や応答時間に与えるボックス化オーバーヘッドの影響を、計測することができます。また、コード内のどの部分がボックス化やボックス化解除による影響を受けているかについて正確に調べるために、MSIL コードの静的分析を行うことができます。以下のコマンド ラインを使い、MSIL 内で box 指示と unbox 指示を探してください。

Ildasm.exe yourcomponent.dll /text | findstr box
Ildasm.exe yourcomponent.dll /text | findstr unbox

ただし、厳密にどの部分でボックス化オーバーヘッドを最適化させるかについては、注意しなければなりません。オーバーヘッドは、ループ、挿入、コレクション内の値型の回収などの反復処理が多くなる部分で大きくなります。ボックス化が 1 度か 2 度しか発生しないインスタンスでは、最適化を図る必要はありません。

Visual Basic .NET コードでは DirectCast を使う。

CType の代わりに DirectCast 演算子を使い、継承階層をキャスト アップおよびキャスト ダウンしてください。DirectCast は MSIL に直接コンパイルされるため、パフォーマンスの向上に役立ちます。また、2 つの型の間に継承関係が存在しない場合、DirectCast は InvalidCastException をスローすることに注意してください。

例外管理

try / catch ブロックを使った構造化された例外処理は、マネージ コードにおける例外的なエラー状況を処理する上での推奨方法です。また、finally ブロック (または C# の using ステートメント) を使用し、例外発生時においてもリソースをクローズするようにすべきです。

堅牢で保守性の高いコードを作成するためには、例外処理は望ましいアプローチですが、それに伴ってパフォーマンス面でのコストも発生します。このため、例外は例外的な状況でのみ使用するべきで、通常のロジック フローを制御するために用いるべきではありません。おおよその目安として、例外パスを通るのは、1000 回に 1 度未満とすべきです。

ここでは、適切な例外処理を行っているかを見直すためのガイドラインを提示します。

  • アプリケーション フローを制御するために例外を使わない。
  • 検証コードを使って不必要な例外を避ける。
  • finally ブロックを使ってリソースが開放されるようにする。
  • Visual Basic .NET の On Error Goto コードを例外処理に置き換える。
  • 処理できない例外をキャッチしない。
  • 再スローは高くつくことを意識する。
  • 例外ハンドラにできる限り多くの診断情報を保存する。
  • パフォーマンス モニタを使って CLR 例外を監視する。

アプリケーション フローを制御するために例外を使わない

例外のスローは、大きな負担を伴います。アプリケーション フローを制御するために例外を使わないでください。コードの通常処理の間に一連のイベントが発生することを予想できる場合は、原則として例外をスローするべきではありません。

以下のコードでは、供給製品が見つからなかった場合に、好ましくない例外のスローが発生してしまいます。

static void ProductExists( string ProductId)
{
//... 製品を検索
if ( dr.Read(ProductId) ==0 ) // レコードが見つからなければ作成を指示
{
throw( new Exception("Product Not found"));
}
}

製品を見つけられない状況は予想可能なため、メソッドの実行結果を示す値を返すように、コードをリファクタリングしてください。以下のコードでは、戻り値を使って顧客アカウントの有無を示しています。

static bool ProductExists( string ProductId)
{
//... 製品を検索
if ( dr.Read(ProductId) ==0 ) // レコードが見つからなければ作成を指示
{
return false;
}
. . .
}

パフォーマンス重視のコード パスやメソッドでは、例外をスローする代わりに、列挙型を使ってエラー情報を返すというプログラミング手法が、よく使われます。

検証コードを使って不必要な例外を避ける

特定の回避可能な状況が発生し得ることが分かっている場合は、それを避けられるようなコードを事前に書いてください。例えば、キャッシュ アイテム使用前に null 確認などの検証チェックを加えると、例外を回避してパフォーマンスを大きく向上させることができます。以下のコードでは、try / catch ブロックを使い、0 で割る計算を処理しています。

double result = 0;
try{
result = numerator/divisor;
}
catch( System.Exception e){
result = System.Double.NaN;
}

コードを以下のように書き直すことにより、例外を回避し、結果的に効率を高めることができます。

double result = 0;
if ( divisor != 0 )
result = numerator/divisor;
else
result = System.Double.NaN;

finally ブロックを使ってリソースが開放されるようにする

正確性を確保し、なおかつパフォーマンスを高めるために、適切な finally ブロックを使い、高くつくリソースがすべて開放されるようにするべきです。このアプローチが正確性だけでなくパフォーマンスにも影響するのは、高くつくリソースをタイミングよく開放することは、多くの場合でパフォーマンス目標値を満たす上で不可欠なためです。

以下のコードにより、接続を確実に閉じることができます。

SqlConnection conn = new SqlConnection("...");
try
{
conn.Open();
// 例外を起こす可能性のある処理を実行
// できる限り早く Close を呼び出す
conn.Close();
// ... 長くなり得る他の処理
}
finally
{
if (conn.State==ConnectionState.Open)
conn.Close(); // 接続を確実に閉じる
}

Close は、try ブロックと finally ブロックの中で呼び出されていることに注意してください。Close を 2 度呼び出しても、例外は発生しません。try ブロックの中で Close を呼び出すと、接続は早く開放され、ベースとなっているリソースを再利用できるようになります。また、finally ブロックは、例外がスローされて try を完了できなくまった場合に、接続を閉じます。この例のように try ブロック内に他の大きな作業が存在する場合、Close を複数回呼び出すのは効果的です。

Visual Basic .NET の On Error Goto コードを例外処理に置き換える

Visual Basic .NET の On Error / Goto によってエラー処理を行うコードを、Try / Catch ブロックを使う例外処理コードに置き換えてください。On Error Goto コードも有効ですが、Try / Catch ブロックを使った方が効率は高く、error オブジェクトの生成も回避できます。

詳細情報

Try / Catch の効率性についての詳細は、MSDN 上の「Visual Basic .NET の例外処理の概要」 を参照してください。

処理できない例外をキャッチしない

例外の詳細についての記録やログが特に必要な場合や、失敗した処理のリトライが可能な場合を除き、例外をキャッチしないでください。値を付加することが可能な場合を除き、例外を任意にキャッチしてはなりません。例外は、呼び出しスタックで、適切な処理を実行可能なハンドラへと上向きに伝達すべきです。

以下のコード例のように、例外全般をキャッチするべきではありません。

catch (Exception e)
{....}

この場合、すべての例外をキャッチすることになります。これらの例外のほとんどは、再スローされます。コードの例外全般をキャッチすると、呼び出しスタックの中身(ローカル変数など)がなくなってしまっているため、例外の元をデバックしにくくなります。

コードが処理可能な例外を、明示してください。これにより、例外のキャッチと再スローを回避できます。以下のコードは、すべての System.IO 例外をキャッチします。

catch ( System.IO )
{
// 例外を判断
}

再スローは高くつくことを意識する

既存の例外を再スローするためにスローを使う場合のコストは、新しい例外をスローするのに伴うコストと、ほぼ同じです。以下のコードでは、既存コードの再スローに伴うコストが、考慮されていません。

try {
// 例外のスローを起こし得る処理...
} catch (Exception e) {
// e について処理
throw;
}

診断情報を追加提供したい場合にのみ、例外をラップして再スローすることを検討すべきです。

例外ハンドラにできる限り多くの診断情報を保存する

処理方法の分からない例外をキャッチし、それを伝えずに終わることがないようにしてください。以下の例で示すように、有用な診断情報を、容易に分かりにくくしてしまう場合があるためです。

try
{
// 例外を発生させるコード
}
catch(Exception e)
{
// 何もしない
}

この結果、エラー コードの診断に有用かもしれない情報が、分かりにくいものになってしまいます。

パフォーマンス モニタを使って CLR 例外を監視する

パフォーマンス モニタを使い、アプリケーションの例外動作を特定してください。.NET CLR 例外オブジェクトについて、以下のカウントを調べてください。

  • # of Exceps Thrown (スローした例外の数) - スローされた例外の総数です。
  • # of Exceps Thrown / sec (1 秒間にスローした例外の数) - 例外のスロー頻度です。
  • # of Finallys / sec (1 秒間の finally 数) - finally ブロックの実行頻度です。
  • Throw to Catch Depth / sec (1 秒間におけるスローからキャッチまでの深さ) - それまでの 1 秒間に例外をスローするフレームから例外を処理するフレームに移ったスタック フレームの数です。

パフォーマンスを向上させるために、アプリケーションにおいて例外をスローする範囲を特定し、例外の数を減らす方法を考慮してください。

詳細情報

例外管理についての詳細は、以下の資料を参照してください。

反復処理とループ処理

アプリケーションでは、一連のステートメントを実行するために、反復処理が何度も用いられます。ループ内のコードを最適化していないと、メモリ消費の増加からCPUの過剰消費まで、パフォーマンス上の重大な問題が発生しかねません。

ここでは、反復とループの効率を向上させるためのガイドラインを提示します。

  • 反復的なフィールド アクセスやプロパティ アクセスを避ける。
  • ループ内の負荷の大きい処理を最適化するか避ける。
  • 頻繁に呼び出されるコードをループ内にコピーする。
  • 再帰処理のループ処理への置き換えを検討する。
  • パフォーマンス重視のコード パスで foreach に代えて for を使用する。

反復的なフィールド アクセスやプロパティ アクセスを避ける

ループ中に静的なデータを使う場合、フィールドやプロパティに繰り返しアクセスしないで、ループ前にそれらを取得してください。以下のコードでは、1 人のカスタマについての注文のコレクションが処理されています。

for ( int item = 0; item < Customer.Orders.Count ; item++ ){
CalculateTax ( Customer.State, Customer.Zip, Customer.Orders[item] );
}

State と Zip はループの間変わらず、以下のコードで示すように、ループのたびにアクセスすることなく、ローカル変数として格納可能であることに注意してください。

string state = Customer.State;
string zip = Customer.Zip;
int count = Customers.Orders.Count;
for ( int item = 0; item < count ; item++ )
{
CalculateTax (state, zip, Customer.Orders[item] );
}

これらがフィールドなら、コンパイラは最適化を自動で実行できる場合があることに注意してください。これらがプロパティなら、自動実行はあまり考えられません。仮想プロパティなら、自動実行は不可能です。

ループ内の負荷の大きい処理を最適化するか避ける

ループ コードの中で、最適化可能な処理を見つけてください。つまり、副次的にボックス化またはアロケーションを発生させるコードを探してください。以下のコードでは、ループのたびに副次的に文字列が作成されます。

String str;
Array arrOfStrings = GetStrings();
for(int i=0; i<10; i++)
{
str+= arrOfStrings[i];
}

以下のコードでは、StringBuilder を使用し、ヒープでの付加的な文字列のアロケーションを避けています。

StringBuilder sb = new StringBuilder();
Array arrOfStrings = GetStrings();
for(int i=0; i<10; i++)
{
sb.Append(arrOfStrings.GetValue(i));
}

以下のガイドラインに従い、ループ内では負荷の大きい処理を避けてください。

  • ループ内でのメソッド呼び出しに注意する。むやみなメソッド呼び出しを避け、適切な場合はインライン コードの使用を検討してください。
  • ループ内での文字列連結について、StringBuilder の使用を検討する。詳細は、この章で後述する「文字列処理」を参照してください。
  • ループを抜け出る、または継続するために複数の条件をテストする場合、ループから抜け出る可能性の最も高いものが最初に動くように、条件を並べる。

頻繁に呼び出されるコードをループ内にコピーする

ループ内からメソッドを繰り返し呼び出す場合は、ループを変更し、呼び出し数を減らすことを検討してください。JIT コンパイラは通常、呼び出しコードが簡単なものなら、インライン化してしまいます。しかし、複雑なシナリオでは、自分自身でコードの最適化を行わなければならない場合がほとんどです。呼び出しのコストは、リモート処理や Web サービスの利用により、プロセス境界やマシン境界を越えるたびに大きくなります。以下は、ループ内で繰り返しメソッドを呼び出すコードの例です。

for ( int item = 0 ; item < Circles.Items.Length; item++ ){
CalculateAndDisplayArea(Circles[item]);
}

以下のアプローチを検討し、呼び出しを減らしてください。

  • 呼び出しコードをループ内に移す。これにより、メソッド呼び出しの数を減らすことができます。
  • 単位作業全体を、呼び出しオブジェクトへ移す。以下のコードでは、呼び出しオブジェクトを改め、ループ全体がリモートで行われるように、必要なデータをすべて渡しています。これにより、ラウンド トリップ数を減らすことができ、リモートでホスト可能なオブジェクトのローカル呼び出しに作業が移されます。
    // 関数を呼び出してすべてのアイテムを格納
    OrderProcessing op = new OrderProcessing();
    StoreAllOrderItems (Order.Items);
    ...
    class OrderProcessing{
    ...
    public bool StoreAllOrderItems ( Items itemsToInsert )
    {
    SqlConnection conn = new SqlConnection(...
    SqlCommnd cmd = new SqlCommand(...
    for ( int item = 0 ; item < orders.Items.Length; item++ ){
    // 注文をデータベースに挿入
    // コマンド オブジェクトで引数をセット
    cmd.ExecuteNonQuery();
    // 注文アイテムを挿入
    }
    }
    . . .
    }
    

再帰処理のループ処理への置き換えを検討する

再帰呼び出しのたびに、スタックにデータが加えられます。コードを調べ、再帰呼び出しを同等のループ処理に置き換えられないかを検討してください。以下のコードでは、再帰呼び出しによって小さな文字列連結タスクを実行しています。

Array arr = GetArrayOfStrings();
int index = arr.Length-1;
String finalStr= RecurStr(index);
string RecurStr(int ind){
if (ind<=0)
return "";
else
return (arr.GetValue(ind)+RecurStr(ind-1));
}

これを書き直した以下のコードでは、連続する呼び出しごとに新しいデータが作成されるのを避け、自身への追加的なメソッド呼び出しが発生しないようにしています。

string ConcString (Array array)
{
StringBuilder sb = new StringBuilder();
for (int i= array.Length; i>0; i--)
{
sb.Append(array.GetValue(i));
}
return sb;
}

パフォーマンス重視のコード パスで foreach に代えて for を使用する

パフォーマンス重視のコードでは、文字列やコレクションの中身の反復に foreach (C#) を使う代わりに、for を使用してください。C# の foreach とVisual Basic .NET の For Each は、文字列やコレクションに拡張ナビゲーションを提供する列挙子を使用します。詳細は、この章で後述する「コレクションに関するガイドライン」の「列挙型オーバーヘッド」を参照してください。

文字列処理

.NET Framework は、文字列を扱うために System.String データ型を提供しています。System.String 型の持つ不変性のため、大規模な文字列操作はパフォーマンスを大きく低下させる場合があります。つまり、文字列データを変更する処理を実行するたびに、メモリにある元の文字列はガベージ コレクションによって後に破棄され、新しい文字列データを保持するための文字列が新たに作成されます。また、String 型は参照型であるため、文字列の中身はマネージ ヒープに格納されることに注意してください。従って、文字列はガベージコレクションによって回収しなければなりません。

ここでは、文字列を扱う際に考慮すべき推奨事項を提示します。:

  • 非効率な文字列連結を避ける。
  • 付加数が既知なら + を使う。
  • 付加数が未知なら StringBuilder を使う。
  • StringBuilder をアキュミュレータとして扱う。
  • 大文字小文字を区別しない文字列比較にはオーバーロードした Compare メソッドを使う。

非効率な文字列連結を避ける

過度の文字列連結は、アロケーション / アロケーション解除処理の増加につながります。文字列変更処理を実行するたびに、新しい文字列が作られ、古い文字列はその後、ガベージ コレクタによって回収されるためです。

  • 文字列リテラルを連結する場合、コンパイラはコンパイル時にそれらを連結します。
    //'Hello' と 'world' は文字列リテラル
    String str = "Hello" + "world";
    
  • 非リテラル文字列を連結する場合、CLR は実行時にそれらを連結します。そのため、+ 演算子を使うと、マネージ ヒープに複数の文字列オブジェクトが生成します。
  • 複雑な文字列操作をする場合や、文字列を複数回連結する場合は、StringBuilder を使ってください。
    // String と '+' を使って付加
    String str = "Some Text";
    for ( ... loop several times to build the string ...) {
    str = str + " additional text ";
    }
    // String と .Append メソッドを使って付加
    StringBuilder strBuilder = new StringBuilder("Some Text ");
    for ( ... loop several times to build the string ...) {
    strBuilder.Append(" additional text ");
    }
    

付加数が既知なら + を使う

付加数が既知で、文字列の連結を1度に行う場合は、+ 演算子を使用するべきです。

String str = str1+str2+str3;

文字列を 1 ステートメントで連結する場合は、String.Concat の呼び出しは 1 度で十分です。これにより、(連結の前に部分連結するための) 一時文字列を持たなくて済みます。

メモ: ループ内や複数の反復処理では、文字列連結に + を使用するべきではありません。代わりに、StringBuilder を使ってください。

付加数が未知なら StringBuilder を使う

ループで反復するときや、動的 SQL クエリを構築するときにあり得るように、付加数が未知の場合は、以下のコード例で示すように StringBuilder クラスを使ってください。

for (int i=0; i< Results.Count; i++)
{
StringBuilder.Append (Results[i]);
}

StringBuilder クラスは、デフォルトでは初期キャパシティ 16 で始まります。初期キャパシティより少ない文字列は、StringBuilder オブジェクトに格納されます。

バッファの初期キャパシティは、以下のオーバーロードしたコンストラクタを使用して設定できます。

public StringBuilder (int capacity);

プリアロケートされたバッファを消費するまでは、追加アロケーションを必要とすることなく、連結を継続できます。結果的に、String オブジェクトよりも StringBuilder オブジェクトを使用した方が、多くの場合で効率が上がります。さらに連結を進める場合、StringBuilder クラスは、現在のキャパシティの 2 倍に相当するサイズの新しいバッファを作成します。

従って、サイズ 16 の StringBuilder で開始し、制限を越えた場合、StringBuilder はサイズ 32 のバッファを新しく割り当て、古い文字列を新しいバッファにコピーします。古いバッファは、アクセス不能となってガベージ コレクションの対象となります。

メモ: StringBuilder の初期キャパシティを最適値に設定し、新しいアロケーションのコストを減らすよう、常に注意すべきです。最適値を判断するための最善の方法は、CLR プロファイラを使ってメモリ消費をトラックすることです。CLR プロファイラの使用法についての詳細は、このガイドの「How To 情報」にある「CLR プロファイラの使用方法」を参照してください。

StringBuilder をアキュミュレータとして扱う

StringBuilder を、アキュミュレータ、または再利用可能なバッファとして扱うことができます。これにより、複数付加の反復中に、一時文字列の割り当てを避けることができます。このアプローチを有効利用できるシナリオの例を、以下に示します。

  • 文字列の連結 - StringBuilder を使って文字列連結を行う場合は、常に以下のアプローチを用いるべきです。
    StringBuilder sb;
    sb.Append(str1);
    sb.Append(str2);
    

    以下のコードよりも、上記のコードを使ってください。

    sb.Append(str1+str2);

    これは、str1 を加え、次にstr2 を加えるために、一時的な str1+str2 を作らなくて済むためです。

  • さまざまな関数からの文字列の連結 - 以下にコード例を示します。
    StringBuilder sb;
    sb.Append(f1(...));
    sb.Append(f2(...));
    sb.Append(f3(...));
    

    上記のコードでは、関数 f1 (...)、f2 (...)、f3 (...) の戻り値のための一時文字列アロケーションが発生します。以下のパターンを使用することにより、これらの一時アロケーションを避けることができます。

    void f1( sb,...);
    void f2( sb,...);
    void f3( sb,...);
    

    この場合、StringBuilder インスタンスは、メソッドの引数として直接渡されます。sb.Append は、関数本体で直接呼び出され、一時文字列のアロケーションは不要になります。

大文字小文字を区別しない文字列比較にはオーバーロードした Compare メソッドを使う

大文字小文字を区別しない文字列比較を実行する場合は、注意が必要です。以下のコードのように ToLower を使うことは、一時文字列オブジェクトが作成されることになるため、避けてください。

// ToLower は一時文字列を生成させるため、大文字小文字を区別しない処理において不適切
String str="New York";
String str2 = "New york";
if (str.ToLower()==str2.ToLower())
// 処理を実行

大文字小文字を区別しない文字列比較をする場合は、Compare メソッドを使った方が効率的です。

str.Compare(str,str2,false);

メモ: String.Compare メソッドは、CultureInfo.CompareInfo プロパティのデータを使い、カルチャを区別する文字列を比較します。.

詳細情報

文字列管理パフォーマンスについての詳細は、MSDN 上の "Improving String Handling Performance in .NET Framework Applications" を参照してください。

配列

配列は、型をグループ化するための基本機能を提供します。どの言語も、独自の配列構文を持っていますが、以下の考慮事項は、どの言語にも当てはまります。

  • 配列は固定サイズを持つ - 配列のサイズは、最初のアロケーションが終わると固定されます。配列のサイズを拡張する必要がある場合は、必要なサイズの配列を新しく作成し、古い配列から要素をコピーしなければなりません。
  • 配列はインデックス付きアクセスをサポートする - 配列内のアイテムへのアクセスには、インデックスを使用できます。
  • 配列は列挙子アクセスをサポートする - foreach コンストラクト (C#) または For Each (Visual Basic .NET) を使って内容を列挙することにより、配列内のアイテムへアクセスできます。
  • メモリは連続使用される - CLRは、配列を連続したメモリ空間に配置します。これにより、アイテムへのアクセスは高速化されます。

ここでは、配列使用時に考慮すべきパフォーマンスに関するガイドラインを提示します。

  • 特別な機能を必要としない限りコレクションよりも配列を使う。
  • 厳密に型指定された配列を使う。
  • 多次元配列の代わりに配列の配列を使う。

特別な機能を必要としない限りコレクションよりも配列を使う

配列は、すべてのコレクションの中で最も速くアクセスできます。そのため、コレクションの動的拡張などの特別な機能を必要としない限り、コレクションよりも配列を使うことを検討すべきです。配列と使うことで、ボックス化およびボックス化解除のオーバーヘッドを避けることもできます。

厳密に型指定された配列を使う

型の格納には、可能な限り、オブジェクト配列よりも、厳密に型指定された配列を使ってください。これにより、配列に格納された型によって型変換やボックス化を行う必要がなくなります。オブジェクトの配列を宣言し、次に整数や浮動点小数などの値型を配列に加えると、以下のコード例で示すようにボックス化オーバーヘッドが発生します。

Object[] array = new Object[10]
arr[0] = 2+3; //boxing occurs here

ボックス化オーバーヘッドを避けるためには、以下のように厳密に型指定された int 配列を宣言してください。

int [] arrIn = new int [10];
arrIn[0] = 2+3;

文字列や独自クラスなどの参照型をオブジェクト配列に格納すると、型キャスト オーバーヘッドが発生します。従って、以下のコード例で示すように、厳密に型指定された配列を使って参照型を格納するべきです。

string[10] arrStr = new string[10];
arrStr[0] = new string("abc");

多次元配列の代わりに配列の配列を使う

配列の配列は、配列の一次元配列です。配列の配列の要素は、異なる次元、サイズのものになり得ます。MSIL によるパフォーマンス最適化のメリットを生かすために、多次元配列の代わりに配列の配列を使ってください。

MSIL は、一次元ゼロベース配列 (SZA 配列) を対象とした指定の指示を備えており、この型の配列へのアクセスは最適化されます。対照的に、多次元配列は、すべての型について、同じ一般コードを使ってアクセスされます。そのため、プリミティブ型の配列では、ボックス化やボックス化解除が必要となります。

メモ: 非ゼロベース配列は、SAZ 配列よりも遅いため、避けてください。

以下のコードでは、配列の配列を宣言し、使用しています。

string[][] Address = new string[2][];    // 文字列の配列の配列
Address[0] = new string[1];
Address[1] = new string[2];
Address[0][0] = "Address [0,1]";
Address[1][0] = "Address [1,0]";
Address[1][1] = "Address [1,1]";
for (int i =0; i <=1; i++) {
for (int j = 0; j < Address[i].Length; j ++)
MessageBox.Show(Address[i][j]);
}

メモ: 配列の配列は共通言語仕様 (CLS) に準拠していないため、言語をまたいで使用できない場合があります。

配列の配列と多次元配列について、それぞれの場合で生成される MSIL コードを調べることにより、効率性を比較することができます。多次元配列を使った以下のコードで、関数呼び出しがどのように発生しているかに注意してください。

int [,] secondarr = new int[1, 2];
secondarr[0, 0] = 40;

上記のコードにより、以下の MSIL が生成されます。関数呼び出しに注目してください。

IL_0029: ldc.i4.s 40
IL_002b: call instance void int32[0...,0...]::Set(int32,
int32,
int32)

以下のコードでは、配列の配列から MSILが生成されます。MSIL の stelem インストラクションが使われていることに注目してください。stelem インストラクションにより、あるインデックスに対する配列要素は、評価スタック上の int32 値に置き換えられます。

int [][] intarr = new int[1][];
intarr[0] = new int[2];
intarr[0][0] = 10;

上記のコードにより、以下の MSIL が生成されます。stelem インストラクションの使用に注目してください。

IL_001c: ldc.i4.s 10
IL_001e: stelem.i4

補足的考慮事項

配列使用時には、以下についても考慮してください。

  • ソート - データベースからデータを取得する場合、クエリで ORDER BY クローズを使ってデータをプリソートできるかを確認してください。追加検索や結果サブセットのソートのために、データベースからの結果をソートして使う必要がある場合、配列のソートが必要となるかもしれません。ソート、SQL クエリの使用、ビジネス層での配列を使ったソートのうち、どのアプローチが最適かを判断するため、常に計測する必要があります。
  • プロパティから Array を返さないようにする。代わりに、インデックス プロパティを使用することを検討してください。
    EmployeeList l = FillList();
    for (int i = 0; i < l.Length; i++) {
    if (l.All[i] == x){...}
    }
    

    上記のコードでは、All プロパティが使われるたびに、文字列の作成と返信が必要となるかもしれません。上記のコードのように、呼び出し元コードがループ内でプロパティを使っている場合は、ループが反復されるたびに文字列が作成されます。

    さらに、メソッドから文字列を返す場合、結果としてできるコードは、直感的に理解しにくいものになります。以下が、そのコード例です。いずれの場合でも、API のために詳細を記録してください。

    // コードの呼び出し
    if (l.GetAll()[i]== x) {...}
    

    コードの一部分から文字列を返さなければならない場合、クライアント間で同期の問題が発生しないように、コピーを返すことを検討してください。

    以下のコードでは、myObj プロパティを呼び出すたびに、配列のコピーが作成されます。その結果、配列のコピーは、DoSomething(obj.myObj[i]) コードを実行するたびに作成されることになります。

    for (int i = 0; i < obj.myObj.Count; i++)
    DoSomething(obj.myObj[i]);
    

コレクションについての説明

コレクションには、リストとディクショナリという 2 つの基本型があります。リストはインデックス ベースです。ディクショナリはキー ベースで、値と共にキーを格納することになります。表 5.3 では、.NET Framework クラス ライブラリが提供する、さまざまなリスト型とディクショナリ型を示しています。

コレクションは、Ienumerable インターフェイス、ICollection インターフェイス、または Ilist インターフェイスを実装した型です。IDictionary を単独で、またはこれら 3 つのインターフェイスに加えて実装している型は、ディクショナリと呼ばれます。表 5.3 では、これらのコレクション型のそれぞれについて、ガイドラインを掲げています。

表 5.3: リスト / ディクショナリ コレクション型

説明
ArrayList動的にサイズを変えられる配列。設計時に必要な配列サイズが不明な場合に効果的です。
Hashtableキーのハッシュ コードに基づいて組成される、キー / 値ペア。検索が必要でもソートは必要ない場合に最適です。
HybridDictionaryコレクションが小さい時は ListDictionary を使い、コレクションが大きくなると Hashtable に切り替えます。
ListDictionary10 以下のキー / 値ペアを格納する場合に効果的です。
NameValueCollectionキーまたはインデックスでアクセス可能な、関連する String キーおよび String 値のソートしたコレクション。
QueueIcollection を実装した、先入れ先出しコレクション。
SortedListキーでソートされ、キーとインデックスでアクセス可能な、キー / 値ペアのコレクション。
Stackオブジェクトの、シンプルな後入れ先出しコレクション。.
StringCollection文字列のための、厳密に型指定された配列のリスト。
StringDictionaryオブジェクトよりも文字列として厳密に型指定されたキーを伴う、ハッシュ テーブル。

コレクションに関する問題

ここでは、コレクションに関連するパフォーマンス上の問題を扱います。

  • ボックス化の問題
  • スレッド セーフティ
  • 列挙型オーバーヘッド

ボックス化の問題

整数や浮動点小数などの値型を格納するために ArrayList などのコレクションを使う場合、あらゆるアイテムが、コレクションに加えるときにボックス化されます (参照型が作成されて値がコピーされます)。コレクションに多数のアイテムを加えると、過大なオーバーヘッドが発生しかねません。以下のコード例では、この問題が発生します。

ArrayList al = new ArrayList();
for (int i=0; i<1000;i++)
al.Add(i); // Add() はオブジェクトをとるので、暗黙的にボックス化される
int f = (int)al[0]; // 要素をボックス化解除

この問題を避けるため、代わりに配列を使うか、特定の値型について独自のコレクション クラスを作成することを検討してください。

メモ: 本稿執筆時点では、.NET Framework 2.0 は、ボックス化 / ボックス化解除 オーバーヘッドを避けるジェネリクスを、C# 言語に導入しています。

スレッド セーフティ

コレクションは通常、デフォルトではスレッド セーフでありません。複数のスレッドからコレクションを読み出すことは可能ですが、コレクションに変更を加えると、そのコレクションにアクセスしているすべてのコレクションは、不特定の結果を受けます。コレクションをスレッド セーフにするには、以下のようにしてください。

  • Synchronized メソッドを使ってスレッド セーフなラッパーを作成し、そのラッパーを通してコレクションに排他的にアクセスする。
    // 新しい ArrayList を作成し初期化
    ArrayList myAr = new ArrayList();
    // コレクションにオブジェクトを追加
    // ArrayList を包む同期ラッパーを作成
    ArrayList mySyncdAr = ArrayList.Synchronized( myAr );
    
  • コレクションへのアクセス時に、SyncRoot プロパティで C# の lock ステートメント (または Visual Basic .NET の SyncLock ) を使う。
    ArrayList myCollection = new ArrayList();
    lock( myCollection.SyncRoot ) {
    // ここにコードを挿入.
    }
    

    同期タイプのコレクションを実装することも可能です。それには、派生コレクションを作成し、SyncRoot プロパティを使って同期メソッドを実装します。このような同期は通常、効率性を損なうため、前述の「ロックと同期に関するガイドライン」を参照し、同期についての注意事項を理解してください。

列挙型オーバーヘッド

.NET Framework version 1.1 のコレクションは、IEnumerable.GetEnumerator をオーバーライドすることによって列挙子を提供します。このアプローチは、以下で掲げる多くの理由により、最適なものとは言えません。

  • GetEnumerator メソッドは仮想メソッドであるため、インライン化できない。
  • 戻し値は、正確な型でなく、IEnumerator インターフェイスである。そのため、正確な列挙子はコンパイル時に分からない。
  • MoveNext メソッドと Current プロパティも仮想メソッド / プロパティであるため、インライン化できない。
  • IEnumerator.Current は、コレクションに格納されたデータ型によってはボックス化およびボックス化解除を必要とする特定のデータ型よりも、System.Object の戻り型を要求する。

これらにより、単純なコレクション型についての foreach 処理に関連するマネージ ヒープ オーバーヘッドと仮想関数オーバーヘッドが発生します。これは、アプリケーションのパフォーマンスを重視する部分に、大きな影響を及ぼし得ます。

オーバーヘッドを最小化するための方法については、次節の「列挙オーバーヘッドを考慮する」を参照してください。

コレクションに関するガイドライン

ここでは、.NET Framework のコレクション型を最も効率的に使い、よく見られるパフォーマンス問題を避けるのに役立つガイドラインを提供します。

  • コレクション型を選ぶ前に要件を分析する。
  • 可能ならコレクションを適切なサイズに初期化する。
  • 列挙オーバーヘッドを考慮する。
  • 楽観的並行処理では IEnumerable を実装する。
  • ボックス化オーバーヘッドを考慮する。
  • foreach の代わりに for を使うことを検討する。
  • 厳密に型指定されたコレクションを実装してキャスト オーバーヘッドを防ぐ。
  • コレクション内のデータへのアクセスを効率化する。

コレクション型を選ぶ前に要件を分析する

コレクションを使う必要はありますか? 一般的に、そして値型を格納する場合は特に、配列の方が効率的です。コレクションを選択するかは、サイズ、格納するデータ型、使用要件に基づいて判断するべきです。以下の判定基準を用いて、コレクションが適切化を判断してください。

  • コレクションをソートする必要はあるか?
  • コレクションで検索する必要はあるか?
  • インデックスによって各要素にアクセスする必要はあるか?
  • カスタム コレクションは必要か?

コレクションをソートする必要はあるか?

コレクションをソートする必要がある場合は、以下を行ってください。:

  • ソートした読み出し専用データをデータ ソースとしてのデータ グリッドに連結するなら、ArrayList を使う。(例えば、データを読み出し専用データ グリッドで表示するために) ArrayList のインデックスを使って読み出し専用データを連結することだけが必要な場合は、この方が SortedList を使用するよりも優れたアプローチです。データは、ArrayList で回収され、ソートされて表示されます。
  • ほとんど静的でまれにしか更新する必要のないデータのソートには、SortedList を使う。
  • 文字列のソートには、NameValueCollection を使う。
  • コレクションを構築する間に SortedList でデータをプリソートする。これにより、ソートされたリストを作成する上での負担は比較的大きくなりますが、既存データの更新や、少量データのリストへの追加に対しては、変更時に自動的かつ効率的な再ソートが行われるようになります。そのため、更新頻度の低い、ほとんど静的なデータについては、Sortedlist が最適です。

コレクションで検索する必要はあるか?

コレクションで検索する必要がある場合は、以下を行ってください。

  • キー / 値ペアに基づいてコレクションでランダム検索を行うなら、Hashtable を使う。
  • 文字列データについてのランダム検索には、StringDictionary を使う。
  • サイズが 10 未満なら、ListDictionary を使う。

インデックスによって各要素にアクセスする必要はあるか

インデックスによって各要素にアクセスする必要がある場合は、以下を行ってください。

  • データへのゼロベース インデックスによるアクセスには、ArrayList と StringCollection を使う。
  • キー名を指定して要素にアクセスするなら、Hashtable、SortedList、ListDictionary、StringDictionary を使う。
  • ゼロベース インデックスを使うか、要素のキーを指定して要素にアクセスするなら、NameValueCollection を使う。
  • 配列により、他のいかなるコレクション型よりも効率的なアクセスを実現できることに注意する。

カスタム コレクションは必要か?

以下の状況に対応するために、カスタム コレクションを作成することを検討してください。

  • 参照によるマーシャリングが必要なら、カスタム コレクションを作成してください。カスタム コレクションは、すべて値渡しされるためです。例えば、サーバーでのみ関連するオブジェクトを、コレクションが格納する場合は、値よりも参照によってコレクションをマーシャリングする方が望ましいかもしれません。
  • カスタム オブジェクトについては、アップキャストもしくはダウンキャスト、またはその両方に伴うコストを避けるため、厳密に型指定したコレクションを作成する必要があります。CollectionBase または Hashtable を継承して厳密に型指定されたコレクションを作成すると、やはりキャストに伴うコストが発生してしまうことに注意してください。これは、内部的には、要素はオブジェクトとして格納されるためです。
  • 読み出し専用コレクションが必要な場合。
  • 厳密に型指定したコレクションについては、独自のシリアル化が必要となります。例えば、Hashtable を拡張すると共に、IDeserializationCallback を実装するオブジェクトを格納している場合は、シリアル化をカスタマイズし、シリアル化プロセス中にハッシュ値の計算を行えるよう、コードを区画化する必要があります。
  • 列挙コストを減らす必要がある場合。

可能ならコレクションを適切なサイズに初期化する

コレクションに格納したいアイテム数を正確に、または大まかにでも知っている場合は、コレクションを適切なサイズに初期化してください。ほとんどのコレクション型は、以下の例で示すように、コンストラクタによるサイズ指定が可能です。

ArrayList ar = new ArrayList (43);

コレクションのサイズを動的に変えられる場合でも、(テストに基づく) 正確な、または大体の初期キャパシティによってコレクションを割り当てた方が効率的です。

列挙オーバーヘッドを考慮する

コレクションは、IEnumerable の実装により、foreach 構文を使った要素の列挙をサポートします。

コレクションでの列挙オーバーヘッドを小さくするには、以下の列挙子パターンの実装を検討してください。

  • IEnumerable.GetEnumerator を実装するなら、非仮想 GetEnumerator メソッドも実装する - クラスの IEnumerable.GetEnumerator メソッドから、この非仮想メソッドを呼び出します。この非仮想メソッドは、以下のコード例のように、ネストされた public 列挙子構造体を返すべきです。
    class MyClass : IEnumerable
    {
    // 独自コレクションで非仮想メソッドを実装
    public MyEnumerator GetEnumerator() {
    return new MyEnumerator(this); // ネストされた public 構造体を返す
    }
    // IEnumerator を実装
    public IEnumerator.GetEnumerator() {
    return GetEnumerator();//call the non-interface method
    }
    }
    

    foreach 構文は、クラスが明示的に提供している場合、クラスの非仮想 GetEnumerator を呼び出します。明示的に提供していない場合は、クラスが IEnumerable から継承されているなら、IEnumerable.GetEnumerator を呼び出します。非仮想メソッドの呼び出しは、インターフェイスを介した仮想メソッドの呼び出しよりも、わずかに効率的です。

  • 列挙子構造体で IEnumerator.Current プロパティを明示的に実装する - .NET コレクションの実装により、プロパティは厳密に型指定されたオブジェクトでなく、System.Object を返します。これにより、キャスト オーバーヘッドが発生します。このオーバーヘッドは、Current プロパティで System.Object でなく、厳密に型指定されたオブジェクトを返すことにより、避けることができます。非仮想 GetEnumerator メソッド (IEnumerable.GetEnumerator でない) を明示的に実装しているため、ランタイムは IEnumerator.Current プロパティを呼び出す代わりに、Enumerator.Current プロパティを直接呼び出すことができます。このため、必要なデータを直接取得できるようになり、仮想関数呼び出しをなくしてインライン化が可能にしたことで、キャスト オーバーヘッドやボックス化オーバーヘッドを回避できます。

    実装は、以下のように行うべきです。

    // クラスでプロパティをカスタマイズ
    // そのプロパティを呼び出してボックス化/キャスト オーバーヘッドを回避
    Public MyValueType Current {
    MyValueType obj = new MyValueType();
    // ここで obj フィールドを取得
    return obj;
    }
    // メンバを明示的に実装
    Object IEnumerator.Current {
    get { return Current} // 非インターフェイス プロパティを呼び出してキャストを回避
    }
    

Enumerator パターンを実装すると、構造上の理由のみのために、1 つの パブリック型 (列挙子) と複数のパブリック メソッドが追加的に発生します。これらの型は、API を感覚的に分かりにくくする他、記録、テスト、バージョン管理などを必要とします。そのため、このパターンは、パフォーマンスが絶対視される場合にのみ使用すべきです。

以下は、このパターンを使用したコード例です。

public class ItemTypeCollection: IEnumerable
{
public struct MyEnumerator : IEnumerator
{
public ItemType Current { get {... } }
object IEnumerator.Current { get { return Current; } }
public bool MoveNext() { ... }
...
}
public MyEnumerator GetEnumerator() { ... }
IEnumerator IEnumerable.GetEnumerator() { ... }
...
}

JIT インライン化のメリットを受けるためには、拡張性がどうしても必要な場合を除き、コレクションの仮想メンバを使わないでください。また、Current プロパティのコードを、現在の値を返すことのみに限定し、インライン化できるようにしてください。または、フィールドを使ってください。

楽観的並行処理では IEnumerable を実装する

IEnumerable インターフェイスを実装する有効な方法は、2 つあります。楽観的並行処理では、コレクションは、列挙中に変更されるものと想定されます。変更されると、InvalidOperationException 例外をスローします。逆に、悲観的アプローチでは、列挙子のコレクションをコピーし、ベースとなるコレクションの変更から列挙子を分離します。 ほとんどの場合、楽観的並行処理の方がパフォーマンスは高くります。

ボックス化オーバーヘッドを考慮する

コレクションに値型を格納するときは、それに関連するオーバーヘッドを考慮すべきです。コレクションのサイズや、データの更新 / アクセス頻度によっては、ボックス化オーバーヘッドは非常に大きくなり得るためです。コレクションの提供する機能が必要でない場合は、ボックス化オーバーヘッドを避けるために配列を使うことを検討すべきです。

foreach の代わりに for を使うことを検討する

パフォーマンス重視のコードで配列またはコレクションの中身を反復する場合は、foreach (C#) の代わりに for を使ってください。foreach の提供する保護が必要でない場合は、特にそうするべきです。

C# の foreach も、Visual Basic .NET の For Each も、配列やコレクションに対する拡張ナビゲーションを、列挙子を使って提供しています。前述したように、.NET Framework が提供するタイプの列挙子の一般的な実装では、その使用に伴い、マネージ ヒープ オーバーヘッドと仮想関数オーバーヘッドが発生します。

パフォーマンス重視のコードでは、これらのオーバーヘッドを回避するため、コレクションの反復には、可能であれば for ステートメントを使うことを検討してください。

厳密に型指定されたコレクションを実装してキャスト オーバーヘッドを防ぐ

厳密に型指定されたコレクションを実装し、アップキャスト / ダウンキャスト オーバーヘッドを避けてください。具体的には、メソッドが一般オブジェクト型でなく、指定型を受け入れ、または返すようにしてください。文字列として厳密に型指定されたコレクションの例としては、StringCollection と StringDictionary があります。

MSDN ライブラリの「Visual Basic と Visual C# の概念」の「チュートリアル: 独自のクラス コレクションの作成」 を参照してください。

コレクション内のデータへのアクセスを効率化する

大量のオブジェクトを処理するときは、各オブジェクトのサイズの管理が、非常に重要です。例えば、1 つの変数に short (Int16)、int/Integer (Int32)、long (Int64) のいずれを使っても違いはほとんどありませんが、コレクションや配列の中にそれらが 100 万個ある場合は、非常に大きな違いが生じ得ます。プリミティブ型と複雑なユーザー定義オブジェクトのいずれを扱っている場合でも、それらのオブジェクトを多数作成する場合は、必要以上にメモリを割り当てないようにしてください。

コレクション型

この節では、以下のコレクション型を使う際に考慮すべき、主な問題を取り上げます。

  • ArrayList
  • Hashtable
  • HybridDictionary
  • ListDictionary
  • NameValueCollection
  • Queue
  • SortedList
  • Stack
  • StringCollection
  • StringDictionary

ArrayList

ArrayList クラスでは、新しいアイテムがリストに追加されて現在のキャパシティを超過すると、リストが動的にサイズを変更します。ArrayList 使用時は、以下の推奨事項を考慮してください。

  • ArrayList は、カスタム オブジェクト型を格納するために、特にデータが頻繁に変わる場合や、挿入/削除処理を頻繁に実行する場合に使う。
  • 望ましいサイズが得られたら (なおかつそれ以上の挿入が予想されなくなったら)、配列リストを正確なサイズにそろえるために、TrimToSize を使う。これにより、メモリ使用も最適化できます。ただし、プログラムがその後、新しい要素を挿入した場合、処理は遅くなります。これは、トリミングでは要素増加の余地が残されないため、ArrayList は動的に増加しなければならなくなるためです。
  • 効率的な検索のため、プリソートしたデータを格納し、ArrayList.BinarySearch を使う。Contains を使ったソートとリニア検索は、高くつきます。これは基本的に、データを 1 度だけソートする場合のアプローチで、頻繁にソートを実行する場合は、SortedList の方が効率的かもしれません。これは、挿入や更新のたびに、コレクション全体の再ソートが自動実行されるようになるためです。
  • 文字列の格納に ArrayList を使わないでください。この場合は、StringCollection を使ってください。.

Hashtable

Hashtable は、キーのハッシュ コードに基づいて組成された、キー / 値ペアのコレクションです。Hashtable 使用時は、以下の推奨事項を考慮してください。

  • Hashtable は、頻繁に変わるかも、変わらないかもしれない、多数のレコードやデータに適する。頻繁に変わるデータは、頻繁に変わらないデータに比べ、ハッシュ値計算のオーバーヘッドが大きくなります。
  • 頻繁に照会されるデータには、Hashtable を使う。例えば、製品 ID をキーとする製品カタログ。

HybridDictionary

HybridDictionary は、コレクションが小さいときは ListDictionary を、コレクションのサイズが大きくなるときは Hashtable を使い、内部で実装されます。以下の推奨事項を考慮してください。

  • ほとんどの間はレコード数が少なく、時々サイズが増加すると予想される場合のデータの格納には、HybridDictionary を使う。コレクションのサイズが常に大きい、または小さいと分かっている場合は、大きければ Hashtable を、小さければListDictionary を使うべきです。これにより、これらのコレクションの両方についてラッパーとして働く HybridDictionary による追加的なコストを、避けることができます。
  • 頻繁に照会されるデータには、HybridDictionary を使う。
  • データのソートに HybridDictionary を使わない。これは、ソートに最適化されていません。

ListDictionary

少量のデータ (10 アイテム未満) の格納には、ListDictionary を使ってください。これは、単リンク リスト実装を使い、IDictionary インターフェイスを実装します。例えば、ファクトリー パターンを実装したファクトリー クラスは ListDictionary を使い、初期化したオブジェクトをキャッシュに格納するかもしれません。これにより、この次に作成要求が発行されたときに、オブジェクトをキャッシュから直接提供できます。

NameValueCollection

関連する文字列キーと、キーまたはインデックスによってアクセス可能な文字列値の、ソートしたコレクションです。例えば、あるクラスの生徒が登録した科目を表示する場合は、NameValueCollection を使うことができます。これにより、生徒名をアルファベット順に並べてデータを格納することができます。

  • キー / 値ペアの文字列をプリソート順に格納するなら、NameValueCollection を使う。同じキーで複数のエントリを得られることにも注意してください。
  • アイテムを定期的に挿入 / 削除する場合、頻繁に変わるデータには NameValueCollection を使う。
  • アイテムをキャッシュし、素早く回収する必要があるなら、NameValueCollection を使う。

Queue

Queue は、先入れ先出しのオブジェクト コレクションです。Queue 使用時は、以下の推奨事項を考慮してください。

  • 優先度に応じてデータに連続的にアクセスするなら、Queue を使う。例えば、飛行機予約要求の待ちリストをスキャンし、キューの初めの乗客に空席を割り当てることによって優先権を与えるアプリケーションです。
  • 先入れ先出しでアイテムを連続的に処理するなら、Queue を使う。
  • 文字列識別子に基づいてアイテムにアクセスするなら、代わりに NameValueCollection を使う。

SortedList

SortedList は、キーによってソートされ、キーとインデックスによってアクセス可能な、キー / 値ペアのコレクションです。新しいアイテムはソートされた順番で加えられ、既存アイテムの位置は、新しいアイテムを収容できるように調整されます。SortedList に関連する作成コストは非常に大きいため、以下の状況で使用すべきです。

  • データがほとんど静的で、一定期間中にわずかなレコードのみを追加または更新する場合。例えば、従業員情報のキャッシュです。これは、従業員数に基づく新しいキーによって更新できます。従業員数は、SortedList で素早く追加されます。一方、ArrayList では、全面的な再ソートが必要となります。従って、SortedList の方がデルタ変更は速くなります。
  • インデックスまたはキーを使った素早いオブジェクトの回収には、SortedList を使う。ソートしたオブジェクトのセットを取得する場合や、特定のオブジェクトを照会する場合に最適です。
  • 大きなデータ変更に SortedList を使用しない。大量データの挿入コストは、大きいためです。代わりに、ArrayList を使用し、Sort メソッドを呼び出してデータをソートしてください。ArrayList は、デフォルトで クイックソート アルゴリズムを使用します。作成やソートに要する時間は、ArrayList の方が SortedList よりもずっと短くなります。
  • 文字列の格納に SortedList を使わない。キャスト オーバーヘッドを避けるためです。代わりに、StringCollection を使ってください。

Stack

単純な後入れ先出しコレクションです。Stack 使用時は、以下の推奨事項を考慮してください。

  • 後入れ先出しでアイテムを処理する場合は、Stack を使う。例えば、一定期間にわたって Web サイトを訪れた、直近の 10 ユーザーをモニタするアプリケーションです。
  • サイズが分かっているなら、初期キャパシティを指定する。
  • 処理後にアイテムを破棄できる場合は、Stack を使う。
  • コレクションの任意アイテムにアクセスする必要が無い場合は、Stack を使う。

StringCollection

文字列のコレクションで、厳密に型指定された ArrayList です。StringCollection 使用時は、以下の推奨事項を考慮してください。

  • 頻繁に変わり、大きなまとまりとして取得する必要のあるデータの格納には、StringCollection を使う。
  • 文字列データの DataGrid への連結には、StringCollection を使う。これにより、データ取得中に文字列をダウンキャストするのに伴うコストを、避けることができます。
  • 文字列のソートや、プリソートしたデータの格納に StringCollection を使わない。

StringDictionary

オブジェクトでなく、文字列として厳密に型指定されたキーを伴う Hashtable です。StringDictionary 使用時は、以下の推奨事項を考慮してください。

  • データが頻繁に変わらないときは、StringDictionary を使う。これは、ベース構造が、厳密に型指定された文字列の格納に使われる Hashtable であるためです。
  • 頻繁に照会する静的文字列の格納には、StringDictionary を使う。
  • タイプ セーフにするために文字列型を保存したい場合、文字列のキー / 値ペアの格納には、Hashtable でなく、常に StringDictionary を使う。

詳細情報

.NET コレクション クラスについての詳細情報は、Microsoft サポート技術情報の Knowledge Base で掲げる以下の文書を参照してください。

リフレクションと遅延バインディング

リフレクションにより、型の調査と比較、メソッドとフィールドの列挙、実行時における型の動的な作成と実行が可能になります。リフレクションは大きなコストを伴いますが、その中でもコストが非常に大きくなる場合があります。上記のうち、コストは最初のリフレクション(型の比較)で最も小さく、最後のリフレクション(動的作成と実行)で最も大きくなります。リフレクションは、アセンブリに含まれたメタデータを調べることで行われます。多くのリフレクション API は、メタデータの検索と解析を伴います。これには、パフォーマンス重視のコードでは避けるべき、特別な処理が必要となります。

遅延バインディングは、リフレクションを内部的に使用するもので、パフォーマンス重視のコードでは避けるべき、コストの大きい処理です。

ここでは、リフレクション コード、または遅延バインディング コードによるパフォーマンスへの影響を、最小限に抑えるための推奨事項を提示します。

  • リフレクションよりも事前バインディングと型の明示を優先する。
  • 遅延バインディングを避ける。
  • パフォーマンス重視のコード パスでは System.Object の使用を避ける。
  • Visual Basic .NET では Explicit と Strict を有効にする。

リフレクションよりも事前バインディングと型の明示を優先する

Visual Basic .NET では、型をオブジェクトとして宣言している場合、暗黙的にリフレクションを使います。一方、C# では、明示的にリフレクションを使います。できる限りリフレクションは使わないようにし、事前バインディングを使うと共に、型を明示的に宣言するべきです。

C# で明示的にリフレクションを使うのは、以下の処理を実行する場合などです。

  • TypeOf、GetType、IsInstanceOfType を使った型比較
  • Type.GetFields を使った動的列挙
  • Type.InvokeMember を使った動的呼び出し

遅延バインディングを避ける

事前バインディングでは、コンパイラにより、必要となる型を特定し、実行時に合わせて最適化することができます。遅延バインディングでは、型特定プロセスは実行時まで行われず、型の特定と初期化のために別途処理指示が必要となります。以下のコードは、型を実行時にロードします。

Assembly asm = System.Reflection.Assembly.LoadFrom("C:\\myAssembly.dll");
Type myType = asm.GetType("myAssembly.MyTypeName");
Object myinstance = Activator.CreateInstance(myType);

この処理は、以下と同様です。

MyTypeName myinstance = new MyTypeName();

場合によっては、型の動的実行が必要となります。しかし、パフォーマンス重視の場合は、遅延バインディングは避けてください。

パフォーマンス重視のコード パスでは System.Object の使用を避ける

System.Object データ型は、あらゆる値型、または参照型を表すことができますが、メソッドの実行やプロパティへのアクセスには、動的呼び出しを必要とします。パフォーマンスが重要なコードでは、Object 型の使用を避けてください。

Visual Basic .NET コンパイラは、型を Object として宣言すると、暗黙的にリフレクションを使用します。

'VB.NET
Dim obj As Object
Set Obj = new CustomType()
Obj.CallSomeMethod()

メモ: これは、Visual Basic .NET に特有の問題です。C# では、このような問題は発生しません。

Visual Basic .NET では Explicit と Strict を有効にする

Visual Basic .NET は、デフォルトでは遅延バインディング コードを許可します。Strict プロパティと Explicit プロパティを true にセットし、Visual Basic .NET が遅延バインディングを許可しないようにしてください。Visual Studio .NET では、Project Properties ダイアログ ボックスを介して、これらのプロパティにアクセスできます。コードのコンパイルにコマンド ライン コンパイラ Vbc.exe を使用している場合は、/optionexplicit フラッグと /optionstrict フラッグを使ってください

コード アクセス セキュリティ

.NET Framework は、コード アクセス セキュリティを提供し、コードによるさまざまな保護リソースおよび保護処理へのアクセスを制御しています。管理者は、ポリシー コンフィギュレーションを通じて、あるアセンブリに何を許可するかをコントロールできます。実行時において、特定のリソースや処理にアクセスしようとすると、許可要求が発行されます。これにより、呼び出しスタック内のあらゆる呼び出し元が、リソースへのアクセスまたは制限付き処理の実行について、正しく許可を受けているか、検証されます。呼び出し元のコードが関連する許可を得ていない場合、セキュリティ例外がスローされます。

セキュリティが要件となっている場合、通常はセキュリティを犠牲にしてパフォーマンスを向上させることはできません。一方、パフォーマンスを犠牲にしてセキュリティを高めることもできません。プラン上で、必要なセキュリティ機能と必要なパフォーマンス機能の両方を提供可能なだけのリソースを確保できていない場合、シンプル化を図るべきかもしれません。パフォーマンスが非常に悪いために実質的に使用できないセキュリティ機能は、存在しないのと同じことで、全体で見れば高くつきます。このような問題は通常、アプリケーションの他のエリアでもかなり見られるため、まずはそれを調査し、チューニングするべきです。セキュリティ機能は、オーバーヘッドに配慮して効率的に利用するようにしてください。

ここでは、アプリケーションのセキュリティ機能を慎重に見直した後に限り、考慮すべきガイドラインを提示します。

  • パフォーマンス重視の信頼済みシナリオでは SuppressUnmanagedCodeSecurity の使用を検討する。
  • 強制的要求よりも宣言的要求を優先する。
  • パフォーマンス重視の信頼済みシナリオでは完全要求よりもリンク要求の使用を検討する。

パフォーマンス重視の信頼済みシナリオでは SuppressUnmanagedCodeSecurity の使用を検討する

P/Invoke COM 相互運用を利用可能な場合、相互運用コードは、呼び出し元コードによるアンマネージ コードの呼び出しについての、呼び出しスタックでの許可要求に依存します。

SuppressUnmanagedCodeSecurity 属性を使うと、スタック使用許可を破棄し、直近の呼び出し元のみをチェックするリンク要求に置き換えることにより、パフォーマンスを向上させることができます。これを行う前に、コードを徹底的に見直し、潜在する攻撃を受けないことを確認するべきです。

以下は、P/Invoke での SuppressUnmanagedCodeSecurity の使用法を示すコードです。

public NativeMethods
{
// ここでの SuppressUnmanagedCodeSecurity の使用は、FormatMessage にのみ適用される
[DllImport("kernel32.dll"), SuppressUnmanagedCodeSecurity]
private unsafe static extern int FormatMessage(
int dwFlags,
ref IntPtr lpSource,
int dwMessageId,
int dwLanguageId,
ref String lpBuffer, int nSize,
IntPtr *Arguments);
}

以下は、COM 相互運用状況での SuppressUnmanagedCodeSecurity の使用法を示すコードです。この属性は、インターフェイス レベルで使用しなければなりません。

[SuppressUnmanagedCodeSecurity]
public interface IComInterface
{
}

詳細情報

リンク要求とその適切な使用方法についての詳細は、MSDN ライブラリの「Web アプリケーション セキュリティ強化: 脅威とその対策」の「コード アクセス セキュリティの実践」の「アンマネージ コード」を参照してください。

強制的要求よりも宣言的要求を優先する

可能な限り、宣言的要求を使ってください。宣言的セキュリティは構文が豊富で、宣言的要求により、.NET Framework はコードを最大限に最適化できるようになります。従って、設計意図を簡潔かつ直接的にコードに反映できるようになります。

パフォーマンス重視の信頼済みシナリオでは完全要求よりもリンク要求の使用を検討する

コードが保護リソースにアクセスする場合や、特権処理を実行する場合は、コードが必要な許可を受けているかを確認するために、コード アクセス セキュリティ 要求が用いられます。完全要求は、ランタイムに対し、コードが必要な許可を受けているかを確認するために、スタック ウォークの実行を求めます。

完全なスタック ウォークは、完全要求の変わりにリンク要求を使うことにより、回避できます。リンク要求は、JIT コンパイル中に直近の呼び出し元のみをチェックするため、パフォーマンスは向上します。ただし、このパフォーマンス向上とセキュリティ要件の間で、バランスをとる必要があります。リンク要求により、コードが潜在する攻撃を受け、悪質なコードに呼び出されて保護リソースへのアクセスや、特権処理の実行を許してしまう危険性は、大幅に増加します。

リンク要求の使用は、パフォーマンス重視の、信頼済みシナリオでのみ検討すべきです。また、セキュリティ上の問題を徹底的に検証した後にのみ、考慮すべきです。

詳細情報

リンク要求とその適切な使用方法についての詳細は、MSDN 上の「Web アプリケーション セキュリティ強化: 脅威とその対策」の「コード アクセス セキュリティの実践」の「リンク要求」 を参照してください。

ワーキング セットに関する考慮事項

ワーキング セットが小さいほど、システム パフォーマンスは向上します。プログラムのワーキング セットとは、そのプログラムの仮想アドレス空間における、最近参照されたページのコレクションをいいます。ワーキング セットのサイズが大きくなるにつれて、メモリ要求は増加します。ワーキング セットのサイズを決める要因には、ロードした DLL 数、プロセス内のアプリケーション ドメイン数、スレッド数、プロセスによって割り当てられたメモリ量、などがあります。アプリケーションの設計時に、以下のポイントを見直してください。

  • アプリケーション起動時間を短縮するため、必要なアセンブリのみをロードする。
  • ロードしているアセンブリを、必要なアセンブリの副次効果とみなす。
  • ユーザーの要求に応じて (ペイ フォー プレイ型)、アプリケーションの初期化、タッチ コード、データを遅らせる。
  • アプリケーション ドメイン数を減らすか、共有アセンブリにする (非共有アセンブリは、各アプリケーション ドメインで 1 度のみロード)。または、その両方を実施する。
  • スレッド数を減らす。効果は小さいものの、各スレッドのスタック、そのスレッドのためのメモリ アロケーション、そのスレッドのために存在するあらゆるコードを除去することにより、ワーキング セットを減らすことができます。このアプローチは、ターミナル サーバー システムで動作するクライアント アプリケーションなど、アプリケーションのコピーを複数作って動作させるシナリオで、特に効果的です。
  • NGen および 非 NGen を試し、どちらがより多くのワーキング セット ページを節約できるかを判定する。Mscorjit.dll により、約 200 KB、コンパイル コストによっては、それ以上を節約可能ですが、完全にネイティブでコンパイルされるアプリケーションは、これをロードしないことに注意してください。一般的には、NGen により、アプリケーションの共有性を向上させる (プライベート ページを減らす) ことができますが、実効速度はわずかに遅くなります (5% 未満の速度減少)。ただし、多くの場合では、ワーキング セットを小さくしたことによる速度の増加は、コードを共有可能にしたことによる速度の減少を上回ります。つまり、ワーキング セットを小さくすることは、速度の向上につながることが多いわけです。

詳細情報

アプリケーションのワーキング セットのサイズは、adump.exe ツールを使って計測できます。詳細は、"Vadump.exe: Virtual Address Dump" を参照してください。

Ngen.exe についての説明

Native Image Generator ユーティリティ (Ngen.exe) により、アセンブリの MSILT 上で JIT コンパイラ を動作させ、ディスクにキャッシュするネイティブ マシン コードを生成させることができます。アセンブリについてのネイティブ イメージが作成された後、アセンブリを動かすたびに、ランタイムはネイティブ イメージを自動的に使用します。アセンブリで Ngen.exe を実行すると、アセンブリによるロードと実行の速度は、潜在的に向上します。これは、コードとデータ構造を動的に生成するのではなく、ネイティブ イメージ キャッシュから回収するようになるためです。

これにより、アプリケーション起動時間が短縮され、ワーキング セットが小さくなる一方で、ランタイムの最適化は損なわれます。コードのパフォーマンスを計測し、Ngen によってアプリケーションに実質的なメリットがもたらされたかを、確認することが重要になります。

起動時間

Ngen.exe によってページを共有し、ワーキング セットを小さくすることで、起動時間を短縮できます。Ngen.exe と 起動時間については、以下のポイントに注意してください。

  • Ngen.exe によってすべてのモジュールをプリコンパイルすると、JIT コンパイルは必要でなくなる。
  • プリコンパイルしたモジュールが既に (部分的に) 常駐していれば、起動時の I/O を減らすことができる。
  • 対応する MSIL よりも多くのコードをプリコンパイルるすことにより、I/O は増加し得る。
  • JIT コンパイルを減らすかなくすことにより、起動時間を短縮できる。
  • 一部のモジュールが Ngen.exe でプリコンパイルされておらず、JIT コンパイルが必要となる場合、起動時間は実質的に長くなリ得る。

ワーキング セット

異なるプロセスの多数のアプリケーション ドメインにロードされる共有アセンブリを使用するアプリケーションにおいて、Ngen.exe により、総メモリ使用量を減らすことができます。.NET Framework version 1.0 および version 1.1 では、Ngen.exe はアプリケーション ドメインをまたいで共有可能なイメージを生成できません。ただし、プロセスをまたいで共有可能なイメージは、生成可能です。オペレーティング システムは、ネイティブでコンパイルされたコードの 1 つのコピーを、全プロセスをたまいで共有できます。一方、JIT コンパイルされたコードは、動的性質を持っているため、プロセスをまたいで共有できません。

Ngen.exe によって完全にプリコンパイルされたコードは、Mscorjit.dll をロードしません。Mscorjit.dll は、アプリケーションのワーキング セットを、約 200 KB 減らします。ネイティブ モジュールは、(.NET Framework 1.0 および 1.1 では) メタデータを含みません。そのため、プリコンパイルされたコードについては、CLR は必要なメタデータと MSIL へのアクセスのため、アセンブリの MSIL バージョンとプリコンパイルしたイメージの両方をロードしなければならないことに注意してください。ただし、プリコンパイルされたイメージを利用可能なら、MSIL と メタデータの必要性は最小限となるため、元の MSIL イメージのそれらの部分は、ワーキング セットに大きく影響しません。

Ngen.exe とワーキング セットについては、以下のポイントに注意してください。

  • Ngen.exe でプリコンパイルされたコードは、共有される可能性がある。一方、JIT コンパイルされたコードは、共有できない。
  • 共有可能なページは、実際に共有される場合にのみ、効果をもたらす。
  • ライブラリや複数インスタンス アプリケーションは、共有による節約を期待できる。
  • (展開またはコードの区画化のために存在する) 単一インスタンス DLL や単一インスタンス EXE は、共有可能性の向上によるメリットを受けられない。

Ngen.exe の実行

Ngen.exe の実行には、以下のコマンド ラインを使用してください。

ngen.exe assemblyname

これにより、指定アセンブリのネイティブ コードを生成することができます。生成されたネイティブ コードは、ネイティブ イメージ キャッシュ、およびグローバル アセンブリ キャッシュ に格納されます。

イメージ キャッシュからのアセンブリの削除は、以下のコードによって実行できます。

ngen.exe /delete assemblyname.

Ngen.exe に関するガイドライン

この節では、Ngen.exe の使用を検討している場合に、考慮すべきガイドラインを提示します。

  • 起動時間重視のシナリオでは起動パスでの Ngen.exe の使用を検討する。
  • アセンブリの共有によるメリットを受けるシナリオでは Ngen.exe を採用する。
  • 共有が限定されるか必要とならないシナリオでは Ngen.exe を使用しない。
  • ASP.NET version 1.0 および version 1.1 で Ngen.exe を使用しない。
  • ASP.NET version 2.0 では Ngen.exe の使用を検討する。
  • Ngen.exe を使用した場合と使用しない場合でそれぞれパフォーマンスを計測する。
  • バージョンが新しくなったらイメージを再生成する。
  • 適切なベース アドレスを選択する。

起動時間重視のシナリオでは起動パスでの Ngen.exe の使用を検討する

起動時間を短縮するためには、Ngen.exe を使用してください。よく見られる使用例としては、応答性を高めるため、または大型のアプリケーションやシステム サービスのパフォーマンスを向上させるため、起動を速くする必要のあるクライアント シナリオが挙げられます。

Ngen.exe は、以下の理由によって起動時間を短縮できます。

  • 使用頻度の低いパスが使われ始めるまで、JIT コンパイルが行われない。
  • メモリでのページ共有が潜在的に可能となる。
  • ディスク キャッシュの利用により、コードのロードが速くなる。

アセンブリの共有によるメリットを受けるシナリオでは Ngen.exe を採用する

ページの共有とワーキング セットの縮小によってメリットを得られるシナリオでは、Ngen.exe の使用が適切です。Ngen.exe は、以下のシナリオなどで役立ちます。

  • ターミナル サーバーで動作する、実行可能なビジネス ライン (複数インスタンス)。
  • 一連のビジネス アプリケーションラインによって用いられる、共有ライブラリ (複数インスタンス)。

共有が限定されるか必要とならないシナリオでは Ngen.exe を使用しない

一般的に、共有が限定されるか、必要とならないシナリオでは、以下の理由によって Ngen.exe によるメリットを受けられません。

  • Ngen.exe への依存により、サービス負荷が発生する。
  • 単一インスタンス アプリケーションまたはライブラリは、ほとんどメリットを受けない。インスタンスが 1 つしかないので、プロセスは、共有可能なコードも共有しません。
  • JIT コンパイラ自体も共有できる。JIT コンパイラをロードすることによる 200 KB のコストは、それを使うアプリケーションの間で割り振られます。

ASP.NET version 1.0 および version 1.1 で Ngen.exe を使用しない

ASP.NET では、Ngen.exe の使用は推奨されていません。Ngen.exe の作成するアセンブリは、アプリケーション ドメイン間で共有できないためです。厳密名付きアセンブリで Ngen.exe を使用すると、ASP.NET 1.0 および 1.1 は、プリコンパイルされたイメージを、必要とする最初のアプリケーション ドメインで使いますが、その後のアプリケーション ドメインは、自身のイメージをロードし、JIT コンパイルします。そのため、パフォーマンス上のメリットを得ることができません。

詳細情報

詳細は、Microsoft サポート技術情報の Knowledge Base 文書 331979 「INFO: ASP.NET は、pre-Just-In-Time ジェネレータ(ngen.exe)ネイティブ イメージを介して(JIT)コンパイルをサポートしません」 を参照してください。

ASP.NET version 2.0 では Ngen.exe の使用を検討する

本稿執筆時点では、.NET Framework 2.0 には、アプリケーション間で共有可能なタイプの Ngen.exe が含まれています。アプリケーション間で共有するアセンブリで、Ngen.exe を使うことを検討してください。Ngen.exe を使用した場合と使用しない場合で、それぞれパフォーマンスを計測するようにしてください。

Ngen.exe を使用した場合と使用しない場合でそれぞれパフォーマンスを計測する

アプリケーションのパフォーマンスを、Ngen.exe を使用した場合と使用しない場合でそれぞれ計測し、メリットを確認してください。この際、ユーティリティの使用とパフォーマンスの向上を直結させるようにしてください。

Ngen.exe は、時に実効速度を犠牲にし、共有化特性を最大限に高めるコードを生成することに注意してください。Ngen.exe は、頻繁に呼び出されるプロシージャについて、実行時パフォーマンスを低下させてしまう場合があります。これは、JIT コンパイラが実行時に提供する最適化の一部に、Ngen.exe によって提供されないものがあるためです。Ngen.exe は、共有可能なコードの生成を優先します。JIT コンパイラには、そのような制約はありません。また、要求に応じてネイティブ イメージを再生成するときに、別途保守が必要となることにも、注意すべきです。

バージョンが新しくなったらイメージを再生成する

バグ修正、更新、外部依存性の変更によってバージョンを新しくした場合は、ネイティブ イメージを再生成するようにしてください。

Ngen.exe は、.NET Framework のバージョン、CPU のタイプ、アセンブリ、ネイティブ コードが生成されたオペレーティング システムなどの情報を提供します。CLR は、その情報に基づくコンパイル後の環境に動作環境が適合しない場合、JIT コンパイルを選択します。

適切なベース アドレスを選択する

最適なパフォーマンスを得るために、適切なベース アドレスを選択してください。Visual Studio .NET 統合開発環境 (IDE) では、(Configuration Properties の Optimization セクションにある) Project Properties ダイアログボックスで、ベース アドレスを指定できます。また、Csc.exe または Vbc.exe コマンド ライン コンパイラでは、/baseaddress オプションを使って指定できます。

アセンブリ同士の衝突を避けるようにしてください。MSIL アセンブリ ファイルの 3 倍のアドレス レンジを割り当てるのが、望ましいアプローチです。バグ修正によるアセンブリ サイズの増加を見込み、予備スペースを確保しておくべきです。

詳細情報

Ngen.exe の使用方法についての詳細は、MSDN ライブラリの「.NET Framework ツール」の「ネイティブ イメージ ジェネレータ (Ngen.exe)」 を参照してください。

まとめ

CLR は、パフォーマンスの高いアプリケーションをサポートするように、高度に最適化され、高度に設計されています。しかし、.NET アセンブリを構築するために用いるコーディング技法により、その高いパフォーマンスからコードが得られるメリットの程度は、変わってきます。この章では、マネージ コード アプリケーションをプログラムする際に考慮すべき、パフォーマンスに関連する主な問題を扱いました。

補足資料

CLR と マネージ コード パフォーマンスについての詳細は、以下の資料を参照してください。

  • 印刷可能なチェックリストについては、このガイドの「チェックリスト」にある「チェックリスト: マネージ コード パフォーマンス」 を参照してください。.
  • 第 13 章「コード レビュー: .NET アプリケーション パフォーマンス」の「マネージ コードと CLR パフォーマンス」
  • 第 15 章「.NET アプリケーション パフォーマンスの計測」の「CLR とマネージ コード」
  • 第 16 章「.NET アプリケーション パフォーマンスのテスト」
  • 第 17 章「.NET アプリケーション パフォーマンスのチューニング」の「CLRのチューニング」

マネージ コード パフォーマンスについての詳細は、以下の資料を参照してください。:


Patterns and Practices ホーム

© 2009 Microsoft Corporation. All rights reserved. 使用条件  |  商標  |  プライバシー
Page view tracker