ガベージ コレクタの基本とパフォーマンスのヒント

Rico Mariani
Microsoft Corporation

April 2003
日本語版最終更新日 2003 年 7 月 8 日

要約 : .NET ガベージ コレクタは、メモリの効率的な使用を実現し、長期的な断片化を解消する、高速なメモリ割り当てサービスを提供します。この記事では、まず、ガベージ コレクタがどのように機能するのかを説明し、次に、ガベージ コレクションを行っている環境で発生する可能性があるパフォーマンス上の問題について解説します。

適用対象 :
Microsoft® .NET Framework

目次

はじめに
単純化したモデル
ガベージ コレクション
パフォーマンス
ファイナライゼーション
まとめ

はじめに

ガベージ コレクタを効果的に活用する方法や、ガベージ コレクションを行っている環境で発生する可能性があるパフォーマンス上の問題について理解するには、まず、ガベージ コレクタがどのように機能するのか、そしてその内部の働きがプログラムの実行にどのような影響を与えるのかについて、その基本を理解する必要があります。

この記事は 2 つの部分に分かれています。前半では、共通言語ランタイム (CLR) のガベージ コレクタの特性について、単純化したモデルを使って大まかに説明します。後半では、その構造に伴うパフォーマンス上の問題について説明します。

単純化したモデル

ここでは、説明のために、以下の単純化したマネージ ヒープのモデルを使用します。実際に実装されているものとは異なることに注意してください。

ms973837.dotnetgcbasics_01(ja-jp,MSDN.10).gif

図 1. 単純化したマネージ ヒープのモデル

この単純化したモデルには、以下の規則があります。

  • ガベージ コレクションの対象となるすべてのオブジェクトに、1 つの連続したアドレス空間が割り当てられます。
  • ヒープは複数の "ジェネレーション" (詳細については以降を参照) に分割されるため、ヒープの一部を確認するだけでほとんどのガベージを除去できます。
  • 1 つのジェネレーション内のオブジェクトは、すべてほぼ同じ世代です。
  • 番号の大きいジェネレーションによって表されるヒープ領域には、古いオブジェクトが含まれています。古いオブジェクトは、新しいオブジェクトに比べて、安定している可能性がはるかに高くなります。
  • 最も古いオブジェクトは最も低いアドレスにあり、新しいオブジェクトが作成されるたびにアドレスが増加します。したがって、上の図 1 では、下にいくにつれてアドレスが増加します。
  • 新しいオブジェクトのための割り当てポインタは、使用されているメモリ領域 (割り当て済みのメモリ領域) と未使用のメモリ領域 (空きメモリ領域) との境界を示します。
  • ヒープは定期的に圧縮されます。これは、不要なオブジェクトを削除して、必要なオブジェクトをヒープの低いアドレスの方にずらすことによって行われます。これにより、図の一番下の、新しいオブジェクトが作成される未使用の領域が拡張されます。
  • メモリ内のオブジェクトの順序は、局所性を高めるために、作成された順序のまま維持されます。
  • ヒープ内のオブジェクトの間にすきまはありません。
  • "コミット" されるのは空き領域の一部だけです。 必要があれば、"予約済み" のアドレス範囲にさらに多くのメモリがオペレーティング システムから取得されます。

ガベージ コレクション

まず、最もわかりやすい、完全な圧縮を行う種類のガベージ コレクションについて説明します。

完全なガベージ コレクション

完全なガベージ コレクションでは、プログラムの実行を中止して、GC ヒープへの "ルート" をすべて見つける必要があります。 これらのルートにはさまざまな形態がありますが、ヒープ内を指すスタック変数やグローバル変数がその最も顕著な例です。ルートから始めてすべてのオブジェクトを確認し、それらのオブジェクトに含まれるすべてのオブジェクト ポインタを辿りながら、オブジェクトをマークしていきます。このようにして、ガベージ コレクタは、"到達可能な" ("必要な") オブジェクトをすべて見つけます。 それ以外の "到達不可能な" オブジェクトは、"廃棄" されます。

ms973837.dotnetgcbasics_02(ja-jp,MSDN.10).gif

図 2. GC ヒープへのルート

到達不可能なオブジェクトを特定できたら、次に、後で使用できるようにその領域を回収する必要があります。この時点のガベージ コレクタの目的は、"必要な" オブジェクトをずらして無駄な領域をなくすことです。 プログラムの実行は中断されているため、すべてのオブジェクトを安全に移動できます。その後、移動先の新しい場所ですべてのオブジェクトが正しくリンクされるように、すべてのポインタを修正します。残ったオブジェクトのジェネレーション番号は 1 つ上がります (つまり、ジェネレーションの境界が更新されます)。その後、実行が再開されます。

部分的なガベージ コレクション

残念ながら、完全なガベージ コレクションは、毎回実行するにはコストが大きすぎます。そこで、先に触れたジェネレーションを利用することになります。ここでは、ジェネレーションがどのように役立つのかを説明します。

まず、きわめて幸運な状況を例に取ります。最近完全なガベージ コレクションが実行されたために、ヒープがきちんと圧縮されていたとします。その後、プログラムの実行が再開され、メモリの割り当てが行われました。その後も多くの割り当てが行われ、やがてメモリ管理システムが、ガベージ コレクションを実行する時期になったと判断しました。

ここで、幸運な状況を想定することになります。前回のガベージ コレクション以降のプログラムの実行で、古いオブジェクトに対する書き込みは一切行われておらず、新たに割り当てられた "ジェネレーション 0 " (gen0) のオブジェクトへの書き込みだけが行われたとします。このような状況は、ガベージ コレクションのプロセスを大幅に簡略化できるという意味でとても好都合です。

このような状況では、通常の完全なガベージ コレクションを行う代わりに、古いオブジェクト (gen1 や gen2) はすべてまだ必要であると見なすことができます。少なくとも、それらのオブジェクトをわざわざ確認する価値はないほど多くのオブジェクトがまだ必要なはずです。さらに、先ほど述べたように、それらのオブジェクトに対する書き込みは一切行われていないため、古いオブジェクトから新しいオブジェクトへのポインタもありません。したがってここでは、まず通常どおりにすべてのルートを確認し、古いオブジェクトを指しているルートがあれば無視します。その他のルート (gen0 を指すルート) に対しては、通常どおりに、すべてのポインタを辿ります。ポインタが古いオブジェクトを指していた場合、そのポインタは無視します。

このプロセスが完了すると、古いジェネレーションのオブジェクトを一切確認することなく、gen0 の必要なオブジェクトをすべて確認したことになります。その後、gen0 のオブジェクトを通常どおりに廃棄して、その分のメモリ領域をずらすことができます。古いオブジェクトには何の影響もありません。

ここで想定した状況は、不要な領域のほとんどが、動きの激しい新しいオブジェクトにあることがわかっているという点で、きわめて好都合な状況です。多くのクラスは、戻り値のための一時オブジェクト、一時的な文字列、およびその他のさまざまなユーティリティ クラス (列挙子など) を作成します。gen0 だけを確認するという方法を使用することによって、ごくわずかなオブジェクトを確認するだけで不要な領域のほとんどを簡単に回収できます。

残念ながら、実際には、この方法を使用できるほどうまくはいきません。少なくとも一部の古いオブジェクトは、変更されて新しいオブジェクトを指すようになるものです。そのような場合は、古いオブジェクトを単純に無視するわけにはいきません。

ジェネレーションと書き込みバリアの連携

上のアルゴリズムを実際に使用できるようにするには、古いオブジェクトのどれが変更されているのかを見分ける必要があります。変更されたオブジェクトの場所を記憶するために、"カード テーブル" と呼ばれるデータ構造を使用します。また、このデータ構造を維持するために、いわゆる "書き込みバリア" を使用します。書き込みバリアは、マネージ コードのコンパイラによって生成されます。 ジェネレーション ベースのガベージ コレクションでは、この 2 つの概念が成功の鍵になります。

カード テーブルを実装するにはさまざまな方法がありますが、ビットの配列と考えるのが最も簡単です。カード テーブルの各ビットは、ヒープ上のメモリ範囲を表します。ここでは、このメモリ範囲を 128 バイトとします。プログラムがどこかのアドレスにオブジェクトを書き込むたびに、書き込みバリアのコードが、どの 128 バイト チャンクに書き込まれたのかを計算し、カード テーブルの対応するビットを設定します。

このメカニズムを加えて、もう一度ガベージ コレクションのアルゴリズムを見てみます。gen0 のガベージ コレクションを実行する場合は、古いジェネレーションへのポインタはすべて無視しながら、上で説明したとおりにアルゴリズムを使用できます。しかし、それが完了したら、カード テーブルで変更済みとしてマークされたチャンクにあるすべてのオブジェクトのすべてのオブジェクト ポインタも見つけなければなりません。これらのポインタは、ルートと同じように扱う必要があります。こうしてこれらのポインタも考慮に入れた場合は、gen0 オブジェクトのみのガベージ コレクションを正しく行うことができます。

この方法は、カード テーブルが常にいっぱいになるような場合はまったく役に立ちません。しかし実際には、古いジェネレーションからのポインタが変更されることは比較的少ないため、この方法を使うことによってかなりのコストを節約できます。

パフォーマンス

ガベージ コレクションの動作の基本的なモデルを把握したので、次に、その動作に影響を与えるいくつかの問題について検討します。これにより、ガベージ コレクタのパフォーマンスを最大限に高めるにはどのようなことを避ければよいのかがわかります。

割り当ての回数の問題

これは最も基本的な問題です。ガベージ コレクタによる新しいメモリの割り当ては非常に高速です。上の図 2 からわかるように、通常は、割り当てポインタを移動させて、新しいオブジェクトのための領域を "割り当て済み" の側に作成するだけで済みます。これ以上は、それほど速くなりようがありません。しかし、遅かれ早かれガベージ コレクションが必要になります。すべての条件が同じであった場合、ガベージ コレクションが行われるのは遅い方が望ましいです。したがって、新しいオブジェクトを作成する際には、たとえそのまま作成した方が速くても、それが本当に必要かつ適切なのかを確認する必要があります。

これは、分かりきったアドバイスに聞こえるかもしれません。しかし実際、たった 1 行のコードを書くだけでどれほどの割り当てが行われるのかということは、いとも簡単に忘れてしまうものです。たとえば、なんらかの比較関数を書いていて、キーワード フィールドを持つオブジェクトのキーワードを、順番に、大文字と小文字を区別しないで比較するとします。この場合、最初のキーワードが非常に短い可能性があるため、キーワード文字列全体を単純に比較するわけにはいきません。String.Split を使ってキーワード文字列を分割し、分割後の各断片を、大文字と小文字を区別しない通常の比較を使って順番に比較すればうまくいきそうです。そう考える方も多いのではないでしょうか。

実際のところ、これはあまりいい考えではありません。String.Split は文字列の配列を作成します。つまり、キーワード文字列内に元からあったすべてのキーワードごとに 1 つの新しい文字列オブジェクトと、もう 1 つ配列のためのオブジェクトが作成されます。これは問題です。仮にこれを並べ替えのコンテキストで行ったとすると、大量の比較が行われ、たった 2 行の比較関数によって大量の一時オブジェクトが作成されることになります。 そして、突然ガベージ コレクタが活発に働き始めます。ガベージ コレクションのしくみがどんなに優れていても、これでは大量のクリーンアップを行わざるを得ません。割り当てをまったく必要としない比較関数を書いた方がよいでしょう。

割り当てるサイズの問題

malloc() などの従来のアロケータを使用する際、割り当てのコストが比較的高いことを承知しているプログラマは、多くの場合、コードで malloc() の呼び出しをできるだけ少なくしようとします。その結果、割り当ての合計回数を減らせるように、必要になるオブジェクトを予測して事前に割り当てるなどして、チャンクごとの割り当てが行われます。この事前割り当て済みのオブジェクトをなんらかのプールから手動で管理することによって、一種の高速なカスタム アロケータを作成できます。

マネージ コードの世界では、以下のような理由から、この方法を使う必要性ははるかに少なくなります。

まず、割り当てのコストが非常に低くなっています。従来のアロケータのように空きブロックを探す必要はなく、ただ空き領域と割り当て済み領域の間の境界を動かすだけで済みます。割り当てのコストが低いということは、オブジェクトをプールする最も大きな理由がなくなったということになります。

次に、事前割り当てを行う場合、当然ながら、すぐに必要な量より多くのメモリを割り当てることになります。その結果、本来は必要ないはずのガベージ コレクションが行われることになります。

最後に、ガベージ コレクタは、プログラマが手動で再利用しているオブジェクトの領域を回収することはできません。なぜなら、グローバルな視点から見ると、これらのオブジェクトはすべて (現在使用されていないものも含めて) まだ "必要" なオブジェクトだからです。 使用されていないオブジェクトをすぐに使用できるように手元に置いておくことによって、大量のメモリが無駄になる可能性があります。

事前割り当ては、あらゆる状況で問題になるわけではありません。たとえば、いくつかの特定のオブジェクトを最初にまとめて割り当てる場合などに、事前割り当てを利用できます。しかし、一般的な方法としては、アンマネージ コードのときほどの必要性はありません。

多すぎるポインタの問題

ポインタが網の目のように張り巡らされたデータ構造を作成する場合、2 つの問題があります。第 1 に、オブジェクトへの書き込みが大量に行われます (下の図 3 を参照)。第 2 に、そのデータ構造に対してガベージ コレクションが行われる際に、ガベージ コレクタがすべてのポインタを辿ることになり、オブジェクトの移動があれば、それらのポインタがすべて変更されることになります。データ構造の存続期間が長く、あまり変更されない場合、ガベージ コレクタがこれらのポインタをすべて確認するのは、完全なガベージ コレクション (gen2 レベルのガベージ コレクション) のときだけで済みます。しかし、このようなデータ構造を一時的に作成する場合 (トランザクション処理の一部として作成する場合など) は、このコストがはるかに頻繁に発生することになります。

ms973837.dotnetgcbasics_03(ja-jp,MSDN.10).gif

図 3. 大量のポインタを含むデータ構造

大量のポインタを含むデータ構造には、ガベージ コレクションが行われる時期とは関係ない別の問題もあります。先にも述べたとおり、オブジェクトが作成されるときには、順番に連続してメモリが割り当てられます。これは、たとえばファイルから情報を復元するなどして、巨大な (また、おそらくは複雑な) データ構造を作成する場合に好都合です。さまざまなデータ型が混在していたとしても、メモリ内ですべてのオブジェクトが 1 箇所にまとめられるため、プロセッサがオブジェクトに高速にアクセスできるようになります。しかし、時間が経過してデータ構造が変更されると、新しいオブジェクトを古いオブジェクトにアタッチする必要が出てきます。これらの新しいオブジェクトは、大分後になって作成されているため、もともとメモリ内にあったオブジェクトの近くにはありません。たとえガベージ コレクタによってメモリが圧縮されても、オブジェクトがメモリ内であちこちに動かされるわけではなく、ただ、無駄な領域をなくすために "ずらされる" だけです。その結果生じる不規則な並びが時間の経過と共に悪化して、結局、整然と圧縮された状態を取り戻すためにデータ構造全体を新しく作成し直すことになる場合もあります。この場合、古い不規則なデータ構造は、その後のガベージ コレクタによって廃棄されます。

多すぎるルートの問題

ガベージ コレクションの際には、当然ルートに対しては特別な扱いが必要になります。ガベージ コレクタはルートを列挙し、1 つ 1 つ十分に検討します。大量のルートがあると、たとえ gen0 のガベージ コレクションでもすばやく実行できません。ローカル変数間のオブジェクト ポインタを多く含む深い再帰関数を作成したりすると、実際に多大なコストが発生します。それは、単にそれらのルートをすべて検討しなければならないということだけではありません。それらのルートによって、比較的短い間にばく大な数の gen0 オブジェクトが保持される可能性もあります (以下を参照)。

オブジェクトへの書き込みが多すぎる問題

先にも述べたように、マネージ プログラムがオブジェクト ポインタを変更するたびに書き込みバリアのコードがトリガされます。これは、以下の 2 つの理由で問題になります。

第 1 に、書き込みバリアのコストは、そもそも最初に行おうとしていた操作のコストに匹敵する場合があります。たとえば、なんらかの列挙子クラスで単純な操作を行う場合に、各ステップごとに一部のキー ポインタをメイン コレクションから列挙子に移動する必要があったとします。これは実際、避けたい事態です。なぜなら、それらのポインタをコピーすることのコストは、書き込みバリアによって事実上倍になります。また、その操作は、各ループごとに 1 回以上発生する可能性があります。

第 2 に、実際に古いオブジェクトへの書き込みが行われた場合、書き込みバリアをトリガすることは二重の意味で問題になります。古いオブジェクトが変更されると、上で説明したように、事実上ルートが作成されて、次回のガベージ コレクションの際に確認しなければならないルートが増えることになります。ある程度以上の数の古いオブジェクトが変更されると、ガベージ コレクションの対象を最も新しいジェネレーションのみに限定することによって通常得られる速度の改善が、事実上失われることになります。

もちろんこの 2 つの理由に加えて、あらゆる種類のプログラムで多すぎる書き込みを避ける通常の理由もあります。すべての条件が同じ場合、プロセッサのキャッシュをより効率よく利用するには、読み取りと書き込みの両方について、あまりメモリには触らないようにすることをお勧めします。

中間的な存続期間のオブジェクトが多すぎる問題

最後に、おそらくジェネレーション ベースのガベージ コレクタの最も大きな落とし穴として、厳密には一時オブジェクトとも存続期間が長いオブジェクトとも言えないオブジェクトが数多く作成される問題があります。これらのオブジェクトは、多くの問題を引き起こす可能性があります。というのも、これらのオブジェクトは、最もコストが低い gen0 のガベージ コレクションではクリーンアップされず (まだ必要になるため)、場合によっては gen1 のガベージ コレクションの後まで残り (まだ使用されているため)、その後すぐに不要になるからです。

ここで問題となるのは、いったんオブジェクトが gen2 レベルに達してしまうと、完全なガベージ コレクションでしかそれを取り除くことができないということです。完全なガベージ コレクションはコストが大きいため、ガベージ コレクタはできるだけそれを遅らせようとします。したがって、"中間的な存続期間" のオブジェクトが多数あると、gen2 が (場合によっては驚くほどの速さで) 肥大化する傾向があります。gen2 はなかなかクリーンアップされず、最終的にクリーンアップされるときには、予想をはるかに超えるコストが伴います。

この種のオブジェクトを避けるには、以下のような対策があります。

  1. 使用している一時領域のサイズに十分注意して、割り当てるオブジェクトをできるだけ少なくする。
  2. 存続期間が比較的長いオブジェクトのサイズを最小限に保つ。
  3. スタック上のオブジェクト ポインタ (ルート) の数をできるだけ少なくする。

これらの対策を取れば、gen0 のガベージ コレクションが効率的に行われる可能性が高まり、gen1 もそれほど速く大きくなることはありません。その結果、gen1 のガベージ コレクションの頻度を抑えられます。また、gen1 のガベージ コレクションを行うべきときが来たときには、中間的な存続期間のオブジェクトは既に不要になっています。この時点で回復できれば、それほどのコストは発生しません。

うまくいけば、gen2 のサイズは、操作が比較的安定している間はまったく増えません。

ファイナライゼーション

これまでは、単純化した割り当てモデルを使っていくつかのトピックについて説明してきました。ここでは、話がやや複雑になりますが、もう 1 つの重要な現象であるファイナライザとファイナライゼーションのコストについて説明します。簡単に言うと、ファイナライザは、どのクラスにも指定できるオプションのメンバであり、不要なオブジェクトのメモリがガベージ コレクタによって回収されるときに必ず事前に呼び出されます。C# では、~Class 構文を使ってファイナライザを指定します。

ファイナライゼーションがガベージ コレクションに与える影響

ガベージ コレクタが、本来なら不要なのがファイナライゼーションが必要なオブジェクトを最初に見つけたときには、まだそのオブジェクトの領域を回収することはできません。ガベージ コレクタは、代わりにそのオブジェクトを、ファイナライゼーションが必要なオブジェクトのリストに追加します。さらに、ファイナライゼーションが完了するまで、そのオブジェクト内のすべてのポインタを有効な状態に保つ必要もあります。つまり、ファイナライゼーションが必要なすべてのオブジェクトは、基本的に、ガベージ コレクタにとっては一時的なルート オブジェクトのようなものになります。

ガベージ コレクションが完了すると、"ファイナライゼーション スレッド" が、ファイナライゼーションが必要なオブジェクトのリストを巡回し、ファイナライザを呼び出します。 この操作が完了すると、オブジェクトが改めて不要な状態になり、その後、通常どおりにガベージ コレクションが行われます。

ファイナライゼーションとパフォーマンス

このファイナライゼーションの基本的な理解から、以下の重要な問題を導き出すことができます。

まず、ファイナライゼーションが必要なオブジェクトはその他のオブジェクトよりも存続期間が長くなるという問題があります。実際、"はるかに" 長く存続する可能性があります。 たとえば、ファイナライゼーションが必要なオブジェクトが gen2 にあったとします。ファイナライゼーションがスケジュールされても、オブジェクトは gen2 にあるため、もう一度ガベージ コレクションが行われるまでには、次回の gen2 のガベージ コレクションを待たなければなりません。それまでの期間は、非常に長くなる場合があります。実際、ことがうまく運ぶほど長くなります。 なぜなら、gen2 のガベージ コレクションはコストが大きいため、あまり頻繁に行われない方が望ましいからです。 場合によっては、ファイナライゼーションが必要な古いオブジェクトの領域が回収されるまでに、gen0 のガベージ コレクションが何十回、何百回と行われることもあります。

次に、ファイナライゼーションが必要なオブジェクトは周囲にも影響を与えるという問題があります。内部のオブジェクト ポインタを有効な状態に維持する必要があるため、直接ファイナライゼーションを必要としているオブジェクトがメモリ内にとどまるだけでなく、そのオブジェクトが直接的および間接的に参照しているすべてのオブジェクトもメモリ内に残ることになります。ファイナライゼーションが必要な 1 つのオブジェクトが巨大なオブジェクト ツリーのルートになっていたりすると、そのツリー全体が、上で述べたように場合によっては長期間にわたって、メモリ内にとどまることになります。したがって、ファイナライザをあまり使用しないようにし、使用する場合は、内部のオブジェクト ポインタができるだけ少ないオブジェクトに配置することが大切です。たとえば先ほどのツリーの例では、ファイナライゼーションが必要なリソースを別のオブジェクトに移して、そのオブジェクトへの参照をツリーのルートに残すことによって、この問題を簡単に回避できます。このようにわずかな変更を加えるだけで、メモリ内に残るオブジェクトが 1 つだけになり (小さなオブジェクトならなお望ましい)、ファイナライゼーションのコストを最小限に抑えることができます。

最後に、ファイナライゼーションが必要なオブジェクトはファイナライザ スレッドの作業を作成するという問題があります。ファイナライゼーションのプロセスが複雑な場合、1 つしかないファイナライザ スレッドではそれらのステップを実行するのに長い時間がかかるため、未処理になる作業が発生する可能性があります。その場合、ファイナライゼーションを待って、より多くのオブジェクトがメモリ内に残ることになります。したがって、ファイナライザの作業はできるだけ少なくすることが重要です。また、ファイナライゼーションの間すべてのオブジェクト ポインタが有効な状態に維持されるとはいえ、既にファイナライゼーションが完了したオブジェクトをポインタが指している可能性もあるという点にも注意が必要です。その場合、ポインタはまったく役に立ちません。一般に、ファイナライゼーションのコードでは、たとえポインタが有効であっても、オブジェクト ポインタを辿るのは避けた方が安全です。最も望ましいのは、安全で短いファイナライゼーション コード パスです。

IDisposable と Dispose

IDisposable インターフェイスを実装すると、多くの場合、本来はファイナライゼーションが必要なオブジェクトでそのコストを回避できます。リソースの存続期間をプログラマがわかっている場合は、ガベージ コレクションの代わりにこのインターフェイスを使ってリソースを回収できます。実際、そのような場合はよくあります。もちろん、オブジェクトが単純にメモリのみを使用していて、ファイナライゼーションや処理が必要ないのであれば、それに越したことはありません。しかし、ファイナライゼーションが必要で、オブジェクトを明示的に管理することが簡単かつ有効な場合が多いようなら、IDisposable インターフェイスを実装することによって、ファイナライゼーションのコストを回避する (少なくとも縮小する) ことができます。

C# では、次のように使用します。このパターンはとても便利です。


class X:  IDisposable
{
   public X(…)
   {
   … initialize resources … 
   }

   ~X()
   {
   … release resources … 
   }

   public void Dispose()
   {
// this is the same as calling ~X()
        Finalize(); 

// no need to finalize later
System.GC.SuppressFinalize(this); 
   }
};

Dispose を手動で呼び出すことによって、ガベージ コレクタがオブジェクトを維持してファイナライザを呼び出す必要がなくなります。

まとめ

.NET ガベージ コレクタは、メモリの効率的な使用を実現し、長期的な断片化を解消する、高速なメモリ割り当てサービスを提供します。しかし、最適なパフォーマンスを得るためには、気を付けなければならないこともあります。

アロケータを最大限に活用するには、以下のことを心がける必要があります。

  • 特定のデータ構造で使用するメモリは、すべて (またはできるだけ多く) 同時に割り当てる。
  • 一時的な割り当ては、あまり複雑にならない範囲で、できるだけ回避する。
  • オブジェクト ポインタへの書き込み (特に古いオブジェクトへの書き込み) の回数をできるだけ減らす。
  • データ構造のポインタの密度を減らす。
  • ファイナライザはできるだけ使用しないようにする。使用する場合も、"リーフ" オブジェクト以外には使用しない。そのためには、必要に応じてオブジェクトを分割する。

主要なデータ構造を見直したり、Allocation Profiler などのツールを使ってメモリ使用のプロファイリングを行ったりする通常の対策も、メモリを効果的に使用したりガベージ コレクタを最大限に活用したりするのに大いに役立ちます。


表示: