C#におけるメモリ管理はどのように機能しますか?
C++と比較すると、C#のガーベジコレクタは魔法のように見え、基盤となるメモリについて心配することなく非常に簡単にコードを書くことができます。しかし、パフォーマンスを気にするのであれば、.NETランタイムがどのようにRAMを管理しているかを知ることが、より良いコードを書くのに役立ちます。
値型と参照型
.NETには2種類の型があり、これは基盤となるメモリの扱いに直接影響を与えます。
値の型は、固定サイズのプリミティブ型です。
int,
bool
,
float,
double, など。値渡しで渡されるので、呼び出すと...
someFunction(int arg)引数はコピーされ、新しいメモリ位置として送信されます。
基本的に、値型はスタックに(通常)保存されます。これは主にローカル変数に適用されるもので、ヒープに保存される例外もたくさんあります。ただし、すべての場合において、値型が存在するメモリの位置は、その変数の実際の値を保持しています。
スタックはメモリ内の特別な場所で、デフォルト値で初期化され、拡張できるものです。スタックは後入れ先出し(LIFO)データ構造です。これはバケツのように考えることができます---変数はバケツの上に追加され、スコープから外れると、.NETはバケツに手を入れて下から一つずつ取り出していきます。
スタックははるかに速いですが、それでもRAM内の単なる場所であり、CPUのキャッシュ内の特別な場所ではありません(ただし、ヒープよりも小さいため、キャッシュ内でホットである可能性が非常に高く、パフォーマンス向上に役立ちます)。
スタックはそのLIFO構造からほとんどの性能を得ています。関数を呼び出すと、その関数で定義されたすべての変数がスタックに追加されます。その関数が戻ると、その変数はスコープ外になり、スタックはその関数が置いたすべてのものをクリアします。ランタイムはスタックフレームを使用してこれを管理し、異なる関数のためのメモリブロックを定義します。スタックの割り当ては非常に速く、スタックフレームの最後に単一の値を書き込むだけだからです。
これは「スタックオーバーフロー」という用語が由来する場所でもあり、関数にネストされたメソッド呼び出しが多すぎてスタック全体が満杯になると発生します。
参照型は、通常、サイズが大きすぎる、固定サイズではない、あるいはスタック上に置くには長く生きすぎるためです。通常、これらはインスタンス化されたオブジェクトやクラスの形をとりますが、サイズが異なる配列や文字列も含まれます。
クラスのインスタンスのような参照型は、しばしば次のように初期化されます。
newキーワードは、クラスの新しいインスタンスを作成し、その参照を返します。これをローカル変数に設定することができ、実際にはスタックを使用してヒープ上の位置への参照を保存します。
ヒープは、コンピュータがメモリを使い果たすまで拡張し、満たすことができるため、大量のデータを格納するのに適しています。しかし、整理されておらず、C#では正しく機能するためにガーベジコレクタで管理する必要があります。また、ヒープの割り当てはスタックの割り当てよりも遅いですが、それでもまだかなり速いです。
しかし、これらのルールにはいくつかの例外があり、さもなければ値型と参照型は「スタック型」と「ヒープ型」と呼ばれていたでしょう。
ラムダ関数の外部変数、ローカル変数の
IEnumeratorasync- クラスの値型フィールドは長期的な変数であり、常にヒープに格納されます。それらは参照型にラップされ、その参照型と一緒に保存されます。
- 静的クラスフィールドは常にヒープに保存されます。
- カスタム構造体は値型ですが、リストや文字列などの参照型を含むことができ、これらは通常通りヒープに保存されます。構造体のコピーを作成すると、ヒープ上のすべての参照型の新しいコピーと割り当てが作成されます。
メソッドのブロックとローカル変数はすべてヒープに格納されます。
「参照型はヒープにある」というルールの最も顕著な例外は、次のような使用です。
stackalloc
と共に
Span<T>スタック上の一時配列用にメモリブロックを手動で割り当て、スコープを外れると通常通りスタックからクリーンアップされます。これにより、比較的高価なヒープ割り当てをバイパスし、その過程でガーベジコレクターへの圧力を軽減できます。パフォーマンスは大幅に向上する可能性がありますが、少し高度な機能なので、これを適切に使用してStackOverflow例外を引き起こさない方法についてのガイドを読むことで、さらに学ぶことができます。
ガーベジコレクションとは何ですか?
スタックは非常に整理されていますが、ヒープは散らかっています。それを管理するものがなければ、ヒープ上のものは自動的にクリーンアップされず、アプリケーションがメモリ不足に陥る原因となります。それは、決して解放されないからです。
もちろん、それは問題です。だからこそ、ガベージコレクタが存在します。ガベージコレクタはバックグラウンドスレッドで動作し、定期的にアプリケーションをスキャンして、スタック上にもはや存在しない参照を探します。それは、そのプログラムが参照されているデータに対する関心を失ったことを示します。.NETのランタイムが介入してクリーンアップし、プロセス内でメモリを移動させることによってヒープをより整理されたものにすることができます。
しかし、この魔法には代償が伴います---ガーベジコレクションは遅く、高価です。それはバックグラウンドスレッドで実行されますが、ガーベジコレクションを実行するためにプログラムの実行を一時停止しなければならない期間があります。これはC#でプログラミングする際のトレードオフです; あなたができることは、作成するゴミを最小限に抑えることだけです。
ガーベジコレクタがない言語では、自分で手動で片付ける必要があります。これは多くのケースで速いですが、プログラマーにとってはより面倒です。したがって、ある意味で、ガーベジコレクタは、自動的に床を掃除してくれるルンバのようなもので、起き上がって掃除機をかけるよりも遅いです。




