―― GC とうまく付き合い、巨大データをストレスなくさばく ――
「ガベージコレクタがあるからメモリ管理はお任せでしょ?」
いいえ。限界付近で走らせるときは この “ひと手間” が効きます。
この記事は若干上級者向けです。
はじめに
.NET環境でのメモリ管理は、ガベージコレクション(GC)のおかげで基本的には自動化されています。しかし、大量のデータを扱う場合や高いパフォーマンスが求められる場面では、GCとの付き合い方を理解し、適切な最適化を施すことが重要です。
今回は、ちょっとニッチだけど知っていたら便利な パフォーマンス高速化の 「ひと手間」をまとめてみました。
そもそも「メモリを使い切る」とは?
ここで言う “使い切る” とは
物理 RAM を無駄なく活かしつつ、GC ポーズや断片化によるスローダウンを招かない こととします。
使い切りたい場面 | ハマりがちな壁 | 主な突破口 |
---|---|---|
大量データを一気に処理 (巨大配列・行列演算など) |
Large Object Heap (LOH) 断片化、OutOfMemoryException
|
配列チャンク化で分割し、ArrayPool やSpan でコピーを減らす |
高スループットで常時確保・解放 (リアルタイム処理、ゲームエンジン等) |
頻繁な GC ポーズ |
GC.TryStartNoGCRegion で一時的に GC を止め、オブジェクトプールで確保回数を抑える |
NUMA サーバで並列バッチ | “遠いメモリ” 参照によるレイテンシ | Server GC と NUMA-aware 配置でメモリ局所性を確保 |
物理 RAM に収まらない超巨大データ | そもそも載らない | Memory-Mapped Fileで OS のページングに任せる/データを シャーディングする |
テクニック 7 連発
1. チャンク分割 & Span
目的: 大配列の “増殖” とコピーを防ぎ、GC 負荷を激減させる。
C#
var buffer = new byte[4 * 1024 * 1024]; // 4 MB の固定バッファ
foreach (var _ in Enumerable.Range(0, chunkCount))
{
stream.ReadExactly(buffer); // IO → バッファ
Process(buffer.AsSpan()); // コピーゼロで処理
}
2. ArrayPool
で「借りる→返す」
目的: 毎回 new byte[]
しない—ヒープを汚さず再利用する。
C#
var pool = ArrayPoolbyte>.Shared;
byte[] buf = pool.Rent(1 * 1024 * 1024); // 1 MB 借用
try { /* 使う */ }
finally { pool.Return(buf); } // 必ず返却!
3. LOH 回避は「分割」一択
目的: LOH 断片化を防ぎ、圧縮 GC でメモリを回収しやすくする。
C#
// NG: 8 MB の配列 → LOH
// OK: 8 KB × 1 000 本 → 世代 0 に分散
double[][] data = Enumerable.Range(0, 1_000)
.Select(_ => new double[1_000])
.ToArray();
4. レイテンシ厳禁なら GC.TryStartNoGCRegion
目的: 「ここだけは 0 ms ポーズ」を保証したいリアルタイム区間を作る。
C#
if (GC.TryStartNoGCRegion(256 * 1024 * 1024)) // 256 MB 以内は GC 停止
{
DoRealtimeWork();
GC.EndNoGCRegion();
}
5. Server GC × NUMA aware
目的: 多コア/多ソケット環境で GC スレッドを並列化し、メモリ局所性も確保。
.NET Core 以降は NUMA 対応済み。スレッドをソケットにピン留めするとさらに効果的です。
6. ネイティブメモリ (NativeMemory
)
目的: GC ヒープ外に確保して “動かない大容量バッファ” を置く。
C#
nint ptr = NativeMemory.Alloc(10_000); // malloc
try
{
Spanbyte> span = new(ptr.ToPointer(), 10_000);
// 高速アクセス
}
finally
{
NativeMemory.Free(ptr); // 忘れるとリーク
}
7. メモリマップトファイルで”RAM 超え”
目的: OS のページキャッシュ&遅延読み込みに丸投げし、巨大データを扱う。
C#
using var mmf = MemoryMappedFile.CreateFromFile(
"huge.bin", FileMode.Open, null, 0, MemoryMappedFileAccess.Read);
using var view = mmf.CreateViewAccessor();
view.ReadArray(0, buffer, 0, buffer.Length);
計測なくして最適化なし
ツール | ひと言用途説明 |
---|---|
dotnet-counters |
gc.heap-size など GC 指標を “ながら” で観測 |
PerfView / dotnet-trace | LOH 断片化や GC パフォーマンスの 原因箇所 を特定 |
BenchmarkDotNet | マイクロベンチ & MemoryDiagnoser でアロケーション hot-spot を可視化 |
Tip: 「まず計測、あとでチューニング」が鉄則。数値がない最適化は大抵ムダ足です。
まとめ
- 配列を割る・借りる・再利用する — まず GC 圧を下げよう
- レイテンシが命なら GC を止める or ヒープ外へ逃がす — ただし責任は自分で取る
- RAM 超えは OS に任せる — Memory-Mapped File が強力
- 必ずプロファイル — “速そう” ではなく “速い” を確認してから採用
これらを段階的に組み合わせれば、プロセスに与えられたメモリを無駄なく活かし切る C# アプリ が完成します。ぜひ自分のワークロードに合わせてカスタマイズしてみてください!
参考リンク
おすすめの記事
Views: 0