コンテンツにスキップ

ガベージコレクションとの違い — C#の管理ヒープ・UnityのGC Allocとの比較

一言で言うと

C++は「解放の責任を型と設計で表す」(RAII・所有権)、C#は「解放をランタイムが後で回収する」(GC)。どちらもメモリ管理であり、コストの払い方が違うだけです。この比較を正しく理解すると、UnityのGC Alloc対策がC++の知識で説明でき、逆にC++の設計がC#の経験で腑に落ちます。

C#の管理ヒープの仕組み(概要)

  • 確保は超高速: 管理ヒープはポインタを進めるだけ(前ページのアリーナと同じ方式!)。new 自体はC++のmallocより速い
  • 回収がGCの仕事: 世代別GC——新しいオブジェクト(Gen0)を頻繁に小さく回収し、生き残りを昇格(Gen1→Gen2)。「大抵のオブジェクトはすぐ死ぬ」という経験則(世代仮説)の活用
  • 回収時にコンパクション(生存者を詰め直す)——断片化が起きない代わり、オブジェクトが動く(だからC#ではポインタを直接持てない/pinningが要る)
  • 一時停止: 回収中はスレッドが止まる(または遅くなる)。これがフレームスパイク

UnityのGC(Boehm GC)は上記の.NET GCと違い非世代・非コンパクション(保守的GC)で、より止まりやすい——「UnityでGC Allocが特に嫌われる」実装上の背景です(インクリメンタルGCで一時停止を分割する緩和策はある)。

コスト構造の対比

C++(RAII/所有権) C#(GC)
確保 汎用newは遅め(特化アロケータで速くできる) 速い(bump allocation)
解放 決定的・即時(デストラクタ) 非決定的・まとめて(GC実行時)
支払いのタイミング 常に少しずつ(設計時の頭脳労働+破棄処理) たまにまとめて(GCスパイク)
断片化 起きる(対策は自前 → 前ページ) コンパクションで解消(Unity Boehmは非対応)
循環参照 shared_ptrでは回収不能(weak_ptrで切る) 回収できる(到達可能性ベース)
メモリ以外の資源(ファイル等) 同じ仕組みで管理(RAII) GCは面倒を見ない(Dispose/usingが別途必要)
「うっかり」の結果 リーク/UAF(未定義動作) 延命(論理リーク)/スパイク

重要な対称性: C++の事故は「早く死にすぎる」(UAF)、C#の事故は「死ななさすぎる」(意図せぬ延命・スパイク)。どちらの言語でも「寿命を設計する」ことからは逃げられません。

UnityのGC Alloc対策をC++の語彙で読み直す

Unityの定石 C++での対応物
毎フレームnewしない・リスト再利用 フレームアロケータ/バッファ再利用
オブジェクトプール Object Pool(同じ)
structで確保回避 値型・スタック配置(C++の既定)
文字列連結を避ける・StringBuilder string領域の再利用・reserve
クロージャ/LINQのAlloc回避 ラムダは元々スタック(→ ラムダ)
stackalloc / Span<T> ローカル配列 / std::span

つまり「Unityで最適化のためにやらされていたこと」は、C++の標準的なメモリ設計そのもの。C#を頑張って書くほどC++の考え方に近づいていた、と言えます。

C++にGCを入れる選択肢は?

  • 技術的には存在する(Boehm GC、参照カウントの徹底=Objective-C的方式)。UEはUObjectに限定したマーク&スイープGCを自作している(→ 第10部)——「ゲームオブジェクトの複雑な所有関係」にはGCが合理的、という設計判断
  • 全域GCをC++で使わない理由: 決定性の喪失、ポインタの自由(GCはどこがポインタか知る必要がある)、RAIIとの相性——C++の強みを消してまで得るものが少ない

Unity開発者が誤解しやすい点

  1. 「C++は毎回deleteを書く言語」— 現代C++はRAIIで書かない(→ RAII)。手で書くならC++を間違えている
  2. 「GCがない=リークまみれ」— 型に組み込まれた解放は、手動Disposeより漏れにくい面すらある
  3. 「GCは遅い」— 確保は速い。問題は一時停止のタイミングを選べないこと。リアルタイム性との相性の問題であり、スループットではGCが勝つ場面もある
  4. UEに行くと「C++なのにGCがある」——UObject層だけの話で、通常のC++オブジェクトはRAIIの世界(2層構造を混同しない)

使い分けの視点(まとめ)

  • 「フレームレートの決定性」が最優先(アクション、VR)→ C++的手法(または C# でもゼロアロケ設計)
  • 開発速度・安全性優先のツール・ロジック層 → GCの世界観で書いてよい
  • どちらの世界でも: 寿命のパターン(同寿命・同サイズ・フレーム限り)を見つけて構造化するのが上級者の共通技法

理解度チェック

  1. C#の管理ヒープの確保が速い理由と、その代金はどこで払われますか。
  2. C++とC#の「うっかり」の対称性(早死に/不死)を説明できますか。
  3. UnityのGC Alloc対策とC++のメモリ設計の対応を2つ挙げてください。

演習

自分のUnityプロジェクトで過去にやったGC Alloc対策を3つ思い出し、それぞれ「C++ならどう書くか」(このページの対応表の語彙で)を書いてください。


前: placement new・allocator・pool・arena | カテゴリ目次 | 次: 演習