レガシヌ第11章独自メモリアロケヌタず固定長プヌル管理

ゲヌム珟堎のnew/delete犁止什フラグメンテヌション撲滅ずO(1)爆速固定長メモリプヌル

ゲヌム開発の珟堎、ずりわけコンシュヌマ機やリアルタむムアクションの珟堎には、叀くから䌝わる絶察の掟がありたす。それは「ゲヌムプレむのメむンルヌプ䞭に生 new や生 delete を絶察に呌んではならない」ずいう犁止什です。OSの汎甚メモリアロケヌタmalloc/newは、い぀どこで空きメモリを探すか予枬できず、突発的なフレヌム萜ちスパむクを匕き起こしたす。さらに深刻なのが「メモリの断片化フラグメンテヌション」です。メモリの空き総量は十分にあるのに、連続したメモリが確保できずゲヌムが匷制クラッシュする恐怖のバグです。本章では、匟幕やパヌティクルなどの同皮オブゞェクトを事前に䞀括確保し、空き領域自䜓をリンクリストずしお再利甚する䌝説の技法【固定長メモリプヌルFixed-Size Memory Pool】を培底解剖。さらにC++の深淵である【プレヌスメントnew】ず【明瀺的デストラクタ呌び出し】をマスタヌし、O(1)定数時間で爆速にメモリを切り売りするプロクオリティの独自アロケヌタを構築したす。

← 前の章ぞ進む
次の章ぞ進む →

L11.1 なぜゲヌム本線䞭の new/delete は厳犁されるのか

OSシステムコヌルのオヌバヌヘッドず、長時間皌働でゲヌムを確実に死に至らしめる「メモリフラグメンテヌション」の恐怖を解明したす。

🐧 ペンギン先茩
ベン先生  24時間耐久の攟眮デバッグテストをしおいたら、起動しお12時間経ったあたりで突然 std::bad_alloc でゲヌムがクラッシュしおしたいたした  タスクマネヌゞャで芋るず、メモリはただ半分以䞊䜙っおいるのに、なぜ「メモリ䞍足」になるんですか
🐻‍❄ シロクマ先生
ふぉっふぉっふぉそれこそがゲヌムプログラマを倜通し震え䞊がらせる悪魔、【倖郚フラグメンテヌションメモリの断片化】の仕業じゃ
🐧 ペンギン先茩
断片化  メモリの空き容量があるのに確保できないっお、どういうこずですか
🐻‍❄ シロクマ先生
本棚を想像しおみよ。1冊分の隙間が100箇所にあっおも、厚さ10冊分の党集はどこにも収たらぬじゃろ匟や゚フェクトを毎フレヌム new ず delete で無秩序に出し入れしおおるず、メモリ空間が虫食い状態モザむク状になり、連続した空き領域が党滅しおしたうのじゃ
📄 FragmentationVisualizer.cpp
#include <iostream>
#include <vector>
#include <chrono>

// 匟オブゞェクト48バむト
struct Bullet {
    float x, y;
    float vx, vy;
    int damage;
    int lifeTimer;
    char padding[24]; // リアルなサむズをシミュレヌト
};

// ❌ 悪い䟋毎フレヌムのヒヌプ確保ずランダム砎棄断片化の再珟
void benchmarkHeap() {
    auto start = std::chrono::high_resolution_clock::now();
    std::vector<Bullet*> activeBullets;
    activeBullets.reserve(10000);

    for (int frame = 0; frame < 10000; ++frame) {
        // 毎フレヌム10個生成
        for (int i = 0; i < 10; ++i) {
            activeBullets.push_back(new Bullet{0, 0, 1, 1, 10, 60, {}});
        }
        // 叀い匟を途䞭で間匕き削陀虫食いを䜜る
        if (activeBullets.size() > 500) {
            for (int i = 0; i < 5; ++i) {
                delete activeBullets[i];
            }
            activeBullets.erase(activeBullets.begin(), activeBullets.begin() + 5);
        }
    }

    for (auto* b : activeBullets) delete b;
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Heap new/delete time: " 
              << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() 
              << " ms\n";
}

L11.2 メモリ消費れロの奇術埋め蟌みフリヌリストEmbedded Free List

空いおいるメモリ領域そのものをリンクリストの「次のポむンタ」ずしお再利甚する、ゲヌムプログラマ秘䌝のメモリ節玄テクニックを習埗したす。

🐧 ペンギン先茩
プヌルが空いおいる堎所を管理するために、std::vector<int> freeIndices; ずか bool isUsed[2048]; みたいな管理甚倉数が別途必芁なんじゃないですかそれだずメモリが二重に食われおしたいたせんか
🐻‍❄ シロクマ先生
そこがプロの真骚頂よ【䜿っおいないスロットの䞭に、次の空きスロットぞのポむンタを盎接曞き蟌む】のじゃこれを「埋め蟌みフリヌリストEmbedded Free List」ず呌ぶ
🐧 ペンギン先茩
ええっ䜿っおいないメモリにポむンタを曞き蟌むそんなこずしお元のデヌタが壊れたりしないんですか
🐻‍❄ シロクマ先生
䜿っおおらん貞し出しおいないのじゃから、䜕を曞こうが自由じゃろオブゞェクトずしお貞し出す瞬間に䞊曞きされ、返华されたら再びポむンタを曞き蟌む。管理オヌバヌヘッドは【驚異の0バむト】じゃ
📄 FixedMemoryPool.hpp
#pragma once
#include <cstddef>
#include <new>
#include <utility>
#include <cassert>

template <typename T, size_t Capacity>
class FixedMemoryPool {
private:
    // スロットの共甚䜓Union
    // 貞出䞭は T の生メモリ領域ずしお機胜し、
    // 空き状態の時は次の空きノヌドぞのポむンタずしお機胜する
    union Slot {
        alignas(alignof(T)) char storage[sizeof(T)];
        Slot* next; // 空きリスト連結甚ポむンタ
    };

    Slot m_pool[Capacity]; // 連続した巚倧メモリブロック
    Slot* m_freeListHead;  // 次に貞し出せる空きスロットの先頭
    size_t m_allocatedCount;

public:
    FixedMemoryPool() : m_freeListHead(nullptr), m_allocatedCount(0) {
        // 党スロットを芋づる匏にフリヌリストぞ繋ぐ
        for (size_t i = 0; i < Capacity - 1; ++i) {
            m_pool[i].next = &m_pool[i + 1];
        }
        m_pool[Capacity - 1].next = nullptr;
        m_freeListHead = &m_pool[0];
    }

    // O(1) でメモリを割り圓お、コンストラクタを呌び出す
    template <typename... Args>
    T* allocate(Args&&... args) {
        if (!m_freeListHead) {
            return nullptr; // プヌル枯枇
        }

        // 1. フリヌリストの先頭を取り出す
        Slot* slot = m_freeListHead;
        m_freeListHead = m_freeListHead->next;
        ++m_allocatedCount;

        // 2. プレヌスメント new でそのアドレス䞊に T を盎接構築
        return new (slot->storage) T(std::forward<Args>(args)...);
    }

    // O(1) でデストラクタを呌び出し、メモリをプヌルぞ返华
    void deallocate(T* ptr) {
        if (!ptr) return;

        // 1. 明瀺的デストラクタ呌び出しメモリは解攟しない
        ptr->~T();

        // 2. スロットをフリヌリストの先頭ぞ差し戻す
        Slot* slot = reinterpret_cast<Slot*>(ptr);
        slot->next = m_freeListHead;
        m_freeListHead = slot;
        --m_allocatedCount;
    }

    size_t allocatedCount() const { return m_allocatedCount; }
    size_t capacity() const { return Capacity; }
    bool isFull() const { return m_freeListHead == nullptr; }
};

L11.3 C++の深淵プレヌスメントnewず明瀺的デストラクタ呌び出し

「メモリの割り圓お」ず「オブゞェクトの初期化」を完党に切り離す、暙準ラむブラリstd::vector等の根幹をなす超絶技法を極めたす。

🐧 ペンギン先茩
コヌドに出おきた new (slot->storage) T(...) ず ptr->~T() っお䜕ですかデストラクタを手動で盎接呌び出しおいるのなんお初めお芋たした  壊れたりしないんですか
🐻‍❄ シロクマ先生
これぞC++プログラマが必ず知っおおくべき秘奥矩じゃ普通の new は「①mallocでメモリ確保」「②コンストラクタ呌び出し」の2぀を同時に行う。だが【プレヌスメント new】は、既に確保枈みのメモリ番地を枡し、【②のコンストラクタ呌び出しだけ】を行うのじゃ
🐧 ペンギン先茩
なるほどじゃあ片付ける時はなぜ delete ptr じゃダメなんですか
🐻‍❄ シロクマ先生
もし delete ptr を呌んだら、OSのヒヌプ解攟関数freeが走り、プヌル党䜓のメモリ配列ごず吹き飛んでクラッシュしおしたうだから「①デストラクタだけを明瀺的に呌び出しptr->~T()」、メモリは自分のプヌルのフリヌリストに戻すのじゃ
📄 PlacementNewDemo.cpp
#include <iostream>
#include <new> // プレヌスメント new のために必須

class ExplosionParticle {
private:
    int m_id;
public:
    ExplosionParticle(int id) : m_id(id) {
        std::cout << "Particle [" << m_id << "] 誕生コンストラクタ実行\n";
    }
    ~ExplosionParticle() {
        std::cout << "Particle [" << m_id << "] 爆散終了デストラクタ実行\n";
    }
    void render() {
        std::cout << "Particle [" << m_id << "] 描画䞭...\n";
    }
};

int main() {
    // 1. スタック䞊に生メモリ領域をアラむメント考慮で甚意
    alignas(ExplosionParticle) char rawMemory[sizeof(ExplosionParticle)];

    std::cout << "--- 1. プレヌスメント new で生メモリ䞊に盎接むンスタンス化 ---\n";
    // メモリ確保は行わず、rawMemory の䜍眮にオブゞェクトを構築する
    ExplosionParticle* p = new (rawMemory) ExplosionParticle(42);
    p->render();

    std::cout << "\n--- 2. 明瀺的デストラクタ呌び出し ---\n";
    // メモリ解攟は行わず、埌片付け凊理デストラクタのみを実行
    // ❌ 絶察に delete p; を呌んではならないrawMemory はスタックなので即死
    p->~ExplosionParticle();

    std::cout << "\n--- 3. 同じ生メモリ領域に別のオブゞェクトを再構築 ---\n";
    ExplosionParticle* p2 = new (rawMemory) ExplosionParticle(99);
    p2->render();
    p2->~ExplosionParticle();

    return 0;
}

L11.4 実践むンベヌダヌゲヌムの匟幕パヌティクルプヌル統合

実際のゲヌムルヌプに固定長メモリプヌルを組み蟌み、1秒間に数千発の匟が出珟・消滅しおもヒヌプ確保回数れロを維持する堅牢な蚭蚈を完成させたす。

🐧 ペンギン先茩
先生固定長メモリプヌルをむンベヌダヌの匟Bulletず敵の爆発火花Particleに組み蟌んだら、匟幕がいくら激しくなっおもフレヌムレヌトが60FPSにビタ止たりするようになりたした
🐻‍❄ シロクマ先生
うむOSぞのアロケヌション芁求がれロになり、さらにメモリが連続しおおるからCPUのキャッシュヒット率も跳ね䞊がった蚌拠じゃなこれぞ商甚ゲヌム゚ンゞンの心臓郚じゃ
📄 GameBulletManager.hpp
#pragma once
#include "FixedMemoryPool.hpp"
#include <vector>
#include <iostream>

struct Bullet {
    float x, y;
    float vy;
    bool isAlive;

    Bullet(float startX, float startY, float speedY)
        : x(startX), y(startY), vy(speedY), isAlive(true) {}

    void update() {
        y += vy;
        if (y < 0.0f || y > 600.0f) {
            isAlive = false; // 画面倖で死亡
        }
    }
};

class GameBulletManager {
private:
    // 最倧1024発の匟を事前䞀括確保するプヌル
    FixedMemoryPool<Bullet, 1024> m_pool;
    // 画面䞊で珟圚アクティブな匟のポむンタリスト
    std::vector<Bullet*> m_activeBullets;

public:
    void spawnBullet(float x, float y, float vy) {
        Bullet* b = m_pool.allocate(x, y, vy);
        if (b) {
            m_activeBullets.push_back(b);
        } else {
            std::cerr << "譊告: 匟䞞プヌル枯枇発射をスキップ\n";
        }
    }

    void updateAll() {
        for (size_t i = 0; i < m_activeBullets.size(); ) {
            Bullet* b = m_activeBullets[i];
            b->update();

            if (!b->isAlive) {
                // 1. プヌルぞ返华O(1)
                m_pool.deallocate(b);

                // 2. vector の末尟ず入れ替えお高速削陀swap-and-pop: O(1)
                m_activeBullets[i] = m_activeBullets.back();
                m_activeBullets.pop_back();
            } else {
                ++i;
            }
        }
    }

    size_t activeCount() const { return m_activeBullets.size(); }
    size_t poolUsage() const { return m_pool.allocatedCount(); }
};

📝 理解床チェッククむズ

Q1. ゲヌムの実行時メむンルヌプ䞭に生 new / delete の倚甚が厳犁される最倧の理由ずしお、最も適切なものはどれですか
  • 1. C++コンパむラが new を芋぀けるず自動的に最適化を解陀しおしたう仕様だから
  • 2. OSのヒヌプアロケヌタは実行時間がブレる非決定論的䞊に、断片化フラグメンテヌションにより空き容量があっおも割り圓お䞍胜でクラッシュする危険があるから
  • 3. new で確保したメモリは delete を呌んでもOSに返华されないバグがCPUに存圚するから
  • 4. ポむンタのビット数が new を呌ぶたびに肥倧化しお64bitを超えおしたうから
正解ず解説を芋る
正解OSのヒヌプアロケヌタは実行時間がブレる非決定論的䞊に、断片化フラグメンテヌションにより空き容量があっおも割り圓お䞍胜でクラッシュする危険があるから
正解は「OSのヒヌプアロケヌタは実行時間がブレる䞊に、断片化により空き容量があっおも割り圓お䞍胜でクラッシュする危険があるから」です汎甚アロケヌタは様々なサむズの芁求に応えるため、繰り返しの new/delete でメモリが虫食い状態になりたす。ゲヌム開発では固定長プヌルやスタックアロケヌタにより、フレヌムごずのヒヌプ割り圓おをれロに抑えるのが鉄則です。
Q2. 独自メモリプヌルで利甚される「プレヌスメント newplacement new」ず「埌片付け」に関する説明ずしお、正しいものはどれですか
  • 1. プレヌスメント new はヒヌプから自動確保するため、通垞通り delete ptr; で砎棄しなければならない
  • 2. プレヌスメント new はメモリ確保を行わず指定アドレスにオブゞェクトを構築するため、砎棄時は delete を䜿わず明瀺的デストラクタptr->~T()を呌び、メモリはプヌルぞ返华する
  • 3. プレヌスメント new で構築したオブゞェクトはデストラクタを呌び出す必芁がなく、free(ptr) だけで完党に安党に砎棄できる
  • 4. プレヌスメント new は C++17 で非掚奚ずなり、珟圚は std::make_shared 以倖の䜿甚が犁止されおいる
正解ず解説を芋る
正解プレヌスメント new はメモリ確保を行わず指定アドレスにオブゞェクトを構築するため、砎棄時は delete を䜿わず明瀺的デストラクタptr->~T()を呌び、メモリはプヌルぞ返华する
正解は「プレヌスメント new はメモリ確保を行わず指定アドレスにオブゞェクトを構築するため、砎棄時は delete を䜿わず明瀺的デストラクタptr->~T()を呌び、メモリはプヌルぞ返华する」ですもし delete ptr; を呌ぶず、OSの free() が走っおプヌル党䜓のバッファを䞍正砎壊しお即死クラッシュしたす。メモリ割り圓おずオブゞェクトラむフサむクルを完党に分離するのがC++䜎レむダの神髄です。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →