placement new・allocator・memory pool・arena allocator・fragmentation¶
なぜ自前のメモリ管理が要るのか¶
汎用の new(malloc)は「任意サイズ・任意タイミング・任意スレッド」に応える万能選手で、その分遅く・不定時間で・断片化します。ゲームは「フレーム末尾で全部捨てる」「同サイズを大量に」などパターンが分かっているので、特化アロケータで桁違いに速くできます。
fragmentation(断片化)¶
- 長時間プレイ(ライブゲーム・常駐サーバー)でじわじわ効く。「合計は足りてるのに確保失敗」「アロケータ内の管理コスト増で徐々に遅くなる」
- 対策の方向: 同サイズはプールに(穴が同サイズなので断片化しない)、同寿命はアリーナに(まとめて捨てるので穴が空かない)
placement new(場所を指定して構築)¶
// C++20 — 「メモリ確保」と「オブジェクト構築」を分離する低レベル道具
alignas(Enemy) std::byte buffer[sizeof(Enemy)]; // 確保は自分で済ませた
Enemy* e = new (buffer) Enemy(); // placement new: このbufferの上にコンストラクタだけ実行
e->~Enemy(); // 破棄も分離: デストラクタだけ明示的に呼ぶ(deleteしない!)
すべての自前アロケータの基礎部品です(vectorの内部も「確保→placement new構築」の分離でできている)。C++17以降は std::construct_at / std::destroy_at という整理された道具もあります。
memory pool(同サイズ特化)¶
Object Pool(第4部)の下位層版: 同サイズのブロックをフリーリストで貸し借りします。確保・解放はポインタ操作数回=O(1)、断片化ゼロ。弾・ノード・メッセージなど「同じ型を大量に出し入れ」する対象に。
arena allocator(同寿命特化。линейный/bump allocator とも)¶
// C++20 — ポインタを進めるだけの最速アロケータ(検証済み: samples/arena_allocator.cpp)
class Arena {
public:
explicit Arena(std::size_t bytes) : buffer_(bytes), offset_(0) {}
void* Allocate(std::size_t size, std::size_t align) {
std::size_t p = (offset_ + align - 1) & ~(align - 1); // アラインメント調整
if (p + size > buffer_.size()) return nullptr; // 満杯ポリシーは設計判断
offset_ = p + size;
return buffer_.data() + p;
}
void Reset() { offset_ = 0; } // 全部まとめて「解放」(個別解放はできない/しない)
private:
std::vector<std::byte> buffer_;
std::size_t offset_;
};
- 確保=加算1回(mallocの数百倍速い)。解放は個別にはしない——
Reset()で一括破棄 - 用途: フレームアロケータ(毎フレームの一時データ→フレーム末尾でReset)、ロード時アロケータ(レベルの全データ→アンロードでReset)
- 制約: デストラクタが必要な型は置けない(Resetはデストラクタを呼ばない)or 別途登録して呼ぶ仕組みが要る。「同寿命でPOD的なデータ」が最適な獲物
allocator(STLへの注入口)¶
STLコンテナは確保戦略を差し替えられます: std::vector<T, MyAllocator<T>>。C++17の polymorphic memory resource (pmr) が現代の形:
#include <memory_resource>
std::byte buf[4096];
std::pmr::monotonic_buffer_resource arena(buf, sizeof(buf)); // 標準のアリーナ実装
std::pmr::vector<int> v(&arena); // このvectorの確保はすべてarenaから(型は同じpmr::vectorのまま)
(検証環境GCC 9.2はpmr対応。ただし本Wikiのサンプルは自作Arenaで概念を示しています)
全体地図(どの層の話か)¶
[Object Pool(第4部)] ゲームオブジェクトの再利用(リセット・ハンドル管理込み)
[memory pool / arena] バイト供給の戦略(このページ)
[malloc / new] 汎用ヒープ(OSから貰った領域を管理)
[OS(VirtualAlloc/mmap)] ページ単位の供給
C#/Unityとの対応¶
- C#では確保戦略を差し替えられない(GCヒープ一択)。対抗手段は「確保しない」(構造体、プール、
stackalloc、Span<T>、ArrayPool)——C#のゼロアロケーション技法は、C++のアロケータ選択の代替と整理できる - Unityの
NativeArray+Allocator指定(Allocator.Temp/TempJob/Persistent)は、まさにフレームアロケータ/アリーナの製品化。Tempが高速なのはこのページの理屈 - UEには
FMemStack(フレームアロケータ)、各種プールが内蔵(→ 第10部)
Unity開発者が誤解しやすい点¶
- 「メモリ確保は一瞬」— 汎用mallocはロック・探索を含む不定時間の処理。フレーム中の大量確保はスパイク源(C#のGC Allocと同型の問題がC++にもある)
- 「アロケータ自作は変態技」— フレームアロケータは50行で書けて効果が大きい、ゲーム業界の標準装備
Allocator.Tempの「1フレーム制限」を不便と思っていた人は、このページで理由(Resetモデル)が腑に落ちるはず
使う場面 / 使わない場面¶
- 使う: 計測で確保がボトルネック/スパイク源と判明。同サイズ大量(プール)・同寿命大量(アリーナ)のパターンがある。断片化が問題になる長時間運用
- 使わない: 起動時に確保して以後触らないデータ(汎用newで十分)。パターンのない雑多な確保。計測前
理解度チェック¶
- 断片化への2大対策(同サイズ→プール/同寿命→アリーナ)の理屈を説明できますか。
- placement newが分離する2つのステップは?
- アリーナに「デストラクタが必要な型」を置くと何が問題ですか。
演習¶
samples/arena_allocator.cpp を拡張し、「フレームの一時文字列組み立て」をアリーナ上で行い、Reset で使い回すデモを書いてください。汎用newとの速度比較(100万回確保)も計測してみてください。
前: キャッシュ局所性とAoS/SoA | カテゴリ目次 | 次: GCとの違い(C#管理ヒープ)