ガベージコレクションとの違い — 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開発者が誤解しやすい点¶
- 「C++は毎回deleteを書く言語」— 現代C++はRAIIで書かない(→ RAII)。手で書くならC++を間違えている
- 「GCがない=リークまみれ」— 型に組み込まれた解放は、手動Disposeより漏れにくい面すらある
- 「GCは遅い」— 確保は速い。問題は一時停止のタイミングを選べないこと。リアルタイム性との相性の問題であり、スループットではGCが勝つ場面もある
- UEに行くと「C++なのにGCがある」——UObject層だけの話で、通常のC++オブジェクトはRAIIの世界(2層構造を混同しない)
使い分けの視点(まとめ)¶
- 「フレームレートの決定性」が最優先(アクション、VR)→ C++的手法(または C# でもゼロアロケ設計)
- 開発速度・安全性優先のツール・ロジック層 → GCの世界観で書いてよい
- どちらの世界でも: 寿命のパターン(同寿命・同サイズ・フレーム限り)を見つけて構造化するのが上級者の共通技法
理解度チェック¶
- C#の管理ヒープの確保が速い理由と、その代金はどこで払われますか。
- C++とC#の「うっかり」の対称性(早死に/不死)を説明できますか。
- UnityのGC Alloc対策とC++のメモリ設計の対応を2つ挙げてください。
演習¶
自分のUnityプロジェクトで過去にやったGC Alloc対策を3つ思い出し、それぞれ「C++ならどう書くか」(このページの対応表の語彙で)を書いてください。
前: placement new・allocator・pool・arena | カテゴリ目次 | 次: 演習