土曜日, 7月 5, 2025
土曜日, 7月 5, 2025
- Advertisment -
ホームニューステックニュース【C#】GCだけに任せるな!メモリ最適化攻略ガイド ※上級者向け #.NET - Qiita

【C#】GCだけに任せるな!メモリ最適化攻略ガイド ※上級者向け #.NET – Qiita



【C#】GCだけに任せるな!メモリ最適化攻略ガイド ※上級者向け #.NET - Qiita

―― GC とうまく付き合い、巨大データをストレスなくさばく ――

「ガベージコレクタがあるからメモリ管理はお任せでしょ?」
いいえ。限界付近で走らせるときは この “ひと手間” が効きます。

この記事は若干上級者向けです。

はじめに

.NET環境でのメモリ管理は、ガベージコレクション(GC)のおかげで基本的には自動化されています。しかし、大量のデータを扱う場合や高いパフォーマンスが求められる場面では、GCとの付き合い方を理解し、適切な最適化を施すことが重要です。

今回は、ちょっとニッチだけど知っていたら便利な パフォーマンス高速化の 「ひと手間」をまとめてみました。

そもそも「メモリを使い切る」とは?

ここで言う “使い切る” とは
物理 RAM を無駄なく活かしつつ、GC ポーズや断片化によるスローダウンを招かない こととします。

使い切りたい場面 ハマりがちな壁 主な突破口
大量データを一気に処理
(巨大配列・行列演算など)
Large Object Heap (LOH) 断片化、OutOfMemoryException 配列チャンク化で分割し、ArrayPoolSpanでコピーを減らす
高スループットで常時確保・解放
(リアルタイム処理、ゲームエンジン等)
頻繁な GC ポーズ GC.TryStartNoGCRegionで一時的に GC を止め、オブジェクトプールで確保回数を抑える
NUMA サーバで並列バッチ “遠いメモリ” 参照によるレイテンシ Server GCNUMA-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: 「まず計測、あとでチューニング」が鉄則。数値がない最適化は大抵ムダ足です。

まとめ

  1. 配列を割る・借りる・再利用する — まず GC 圧を下げよう
  2. レイテンシが命なら GC を止める or ヒープ外へ逃がす — ただし責任は自分で取る
  3. RAM 超えは OS に任せる — Memory-Mapped File が強力
  4. 必ずプロファイル — “速そう” ではなく “速い” を確認してから採用

これらを段階的に組み合わせれば、プロセスに与えられたメモリを無駄なく活かし切る C# アプリ が完成します。ぜひ自分のワークロードに合わせてカスタマイズしてみてください!

参考リンク

おすすめの記事





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -