C++ 메모리 풀 고급 기법 | 객체 풀·슬랩 할당자·메모리 아레나 완벽 가이드 [#51-4]

C++ 메모리 풀 고급 기법 | 객체 풀·슬랩 할당자·메모리 아레나 완벽 가이드 [#51-4]

이 글의 핵심

C++ 고급 메모리 관리: 객체 풀, 슬랩 할당자, 메모리 아레나 설계·구현. 프로파일러 병목 해결, 문제 시나리오, 완전한 예제, 흔한 에러, 성능 벤치마크, 프로덕션 패턴까지 실전 코드로 다룹니다.

들어가며: 기본 풀을 넘어선 고급 메모리 관리

”고정 블록 풀만으로는 부족해요”

#48-3 메모리 풀#39-2 std::pmr에서 기본 풀과 표준 할당자를 다뤘습니다. 이 글은 객체 풀(Object Pool), 슬랩 할당자(Slab Allocator), 메모리 아레나(Memory Arena)처럼 더 세밀한 제어가 필요한 고급 기법을 다룹니다.

비유: 기본 풀은 “창고에서 상자 하나씩 꺼내 쓰는 것”이라면, 객체 풀은 “특정 타입 전용 창고”, 슬랩은 “크기별로 나눠진 창고”, 아레나는 “한 번에 큰 공간을 잡고 순차적으로 잘라 쓰는 것”입니다.

이 글을 읽으면:

  • 객체 풀으로 생성자/소멸자 비용까지 줄이는 방법을 알 수 있습니다.
  • 슬랩 할당자로 크기별 최적화를 적용할 수 있습니다.
  • 메모리 아레나로 단편화 없이 순차 할당하는 패턴을 이해할 수 있습니다.
  • 프로덕션에서 어떤 패턴을 선택할지 결정할 수 있습니다.

요구 환경: C++17 이상


문제 시나리오

시나리오 1: 객체 생성/소멸 비용이 누적될 때

"Bullet 객체를 매 프레임 수천 개 생성·파괴하는데, 생성자 호출만 해도 부담이에요."
"placement new는 알겠는데, 타입별로 풀을 어떻게 관리하죠?"

상황: 게임에서 Bullet, Particle 같은 작은 객체를 매 프레임 대량 생성/해제합니다. new/delete 대신 풀을 써도, 매번 생성자·소멸자를 호출하면 오버헤드가 남습니다.

해결 포인트: 객체 풀은 메모리만 재사용하는 게 아니라, 객체의 생성/소멸을 풀과 연동해 “풀에서 꺼낼 때만 생성자, 반환할 때만 소멸자”를 호출합니다.

시나리오 2: 할당 크기가 다양할 때

"32바이트, 64바이트, 128바이트, 256바이트가 섞여서 할당돼요."
"고정 블록 풀 하나로 하면 256바이트 풀에 32바이트 요청 시 낭비가 심해요."

상황: HTTP 파서에서 헤더(작음), 바디 청크(중간), 큰 JSON(큼) 등 크기가 다양합니다. 256바이트 고정 풀에 32바이트만 쓰면 내부 단편화가 큽니다.

해결 포인트: 슬랩 할당자는 32, 64, 128, 256 등 크기 클래스별로 풀을 두어, 요청 크기에 맞는 최소 풀에서 할당합니다.

시나리오 3: 순차 할당만 하고 개별 해제가 없을 때

"요청 처리 중에만 할당하고, 요청 끝에 한 번에 다 해제해요."
"개별 해제가 필요 없어서, 풀의 free list 관리 오버헤드가 아깝습니다."

상황: HTTP 요청 처리, 프레임 렌더링처럼 “할당만 하고 끝에 일괄 해제”하는 패턴입니다. free list를 유지할 필요가 없습니다.

해결 포인트: 메모리 아레나는 커서만 앞으로 이동시키며 순차 할당하고, reset() 한 번으로 전체를 되돌립니다. monotonic_buffer_resource와 같은 개념입니다.

시나리오 4: 스레드별 피크 사용량이 다를 때

"워커 스레드마다 할당 패턴이 달라요. 어떤 스레드는 10MB, 어떤 스레드는 100KB만 써요."
"전역 풀 하나로 하면 락 경합이 있고, 스레드별 풀은 메모리 낭비가 걱정돼요."

상황: 스레드 풀 워커마다 작업 특성이 달라, 스레드별 풀을 두면 일부 스레드 풀은 비어 있고 일부는 부족합니다.

해결 포인트: 계층적 아레나 + 스레드 로컬 풀 조합. 부족 시 상위 풀에서 청크를 가져오는 구조로 메모리 효율과 확장성을 동시에 확보합니다.

시나리오별 권장 패턴

시나리오특징권장 패턴
타입별 대량 생성/해제Bullet, Particle 등객체 풀
크기 다양, 내부 단편화 우려32~256바이트 혼합슬랩 할당자
순차 할당, 일괄 해제HTTP 요청, 프레임메모리 아레나
스레드별 사용량 편차워커 풀계층 아레나 + TLS 풀

목차

  1. 객체 풀 (Object Pool)
  2. 슬랩 할당자 (Slab Allocator)
  3. 메모리 아레나 (Memory Arena)
  4. 완전한 고급 메모리 풀 예제
  5. 자주 발생하는 에러와 해결법
  6. 성능 벤치마크
  7. 프로덕션 패턴
  8. 정리

1. 객체 풀 (Object Pool)

핵심 아이디어

객체 풀은 특정 타입 T 전용으로, 메모리 할당뿐 아니라 객체 생성/소멸을 풀과 연동합니다. allocate 대신 acquire()로 “풀에서 꺼낸 메모리에 placement new로 객체 생성”, deallocate 대신 release(obj)로 “소멸자 호출 후 풀에 반환”합니다.

flowchart TB
    subgraph Pool["객체 풀 (Bullet)"]
        F1[빈 슬롯 1]
        F2[빈 슬롯 2]
        F3[빈 슬롯 3]
    end
    subgraph Acquire["acquire()"]
        A1[빈 슬롯 선택]
        A2["placement new Bullet()"]
        A3[객체 반환]
    end
    subgraph Release["release(obj)"]
        R1["obj->~Bullet()"]
        R2[슬롯을 free list에 반환]
    end
    F1 --> A1 --> A2 --> A3
    A3 --> R1 --> R2 --> F1

기본 객체 풀 구현

// object_pool_basic.hpp
// g++ -std=c++17 -O2 -o object_pool object_pool_basic.cpp
#include <cstddef>
#include <new>
#include <utility>

template <typename T>
class ObjectPool {
public:
    ObjectPool() = default;
    ~ObjectPool() {
        // 아직 반환되지 않은 객체는 소멸되지 않음 — 사용자 책임
        while (head_) {
            Node* next = head_->next;
            ::operator delete(head_);
            head_ = next;
        }
    }

    ObjectPool(const ObjectPool&) = delete;
    ObjectPool& operator=(const ObjectPool&) = delete;

    template <typename... Args>
    T* acquire(Args&&... args) {
        void* mem = allocate_raw();
        return new (mem) T(std::forward<Args>(args)...);
    }

    void release(T* obj) {
        if (!obj) return;
        obj->~T();
        deallocate_raw(obj);
    }

private:
    union Node {
        Node* next;
    };

    void* allocate_raw() {
        if (!head_) expand();
        Node* p = head_;
        head_ = head_->next;
        return p;
    }

    void deallocate_raw(void* p) {
        Node* node = static_cast<Node*>(p);
        node->next = head_;
        head_ = node;
    }

    void expand() {
        constexpr std::size_t blocks_per_chunk = 64;
        std::size_t chunk_size = sizeof(Node) * blocks_per_chunk;
        std::size_t align = alignof(std::max_align_t);
        chunk_size = (chunk_size + align - 1) & ~(align - 1);

        char* chunk = static_cast<char*>(::operator new(chunk_size));
        for (std::size_t i = 0; i < blocks_per_chunk; ++i) {
            Node* node = reinterpret_cast<Node*>(chunk + i * sizeof(Node));
            node->next = head_;
            head_ = node;
        }
    }

    static_assert(sizeof(T) >= sizeof(Node), "T must be at least sizeof(Node*)");
    static_assert(alignof(T) >= alignof(Node), "T alignment must be sufficient");

    Node* head_ = nullptr;
};

주의: 위 구현은 sizeof(T)sizeof(Node*) 이상일 때만 동작합니다. 더 작은 타입은 블록 크기를 max(sizeof(T), sizeof(Node*))로 조정해야 합니다.

사용 예: Bullet 객체 풀

// bullet_pool_example.cpp
#include "object_pool_basic.hpp"
#include <iostream>

struct Bullet {
    float x, y, vx, vy;
    Bullet() : x(0), y(0), vx(0), vy(0) {}
    Bullet(float x_, float y_, float vx_, float vy_)
        : x(x_), y(y_), vx(vx_), vy(vy_) {}
};

int main() {
    ObjectPool<Bullet> pool;

    Bullet* b1 = pool.acquire(100.0f, 200.0f, 5.0f, 0.0f);
    Bullet* b2 = pool.acquire();
    b2->x = 50.0f;
    b2->y = 100.0f;

    pool.release(b1);
    pool.release(b2);

    // 재사용
    Bullet* b3 = pool.acquire(0, 0, 1, 1);
    std::cout << "Reused: " << b3->x << "," << b3->y << "\n";
    pool.release(b3);
    return 0;
}

블록 크기 조정 (작은 타입 지원)

template <typename T>
class ObjectPool {
    // ...
    static constexpr std::size_t block_size_ =
        (sizeof(T) > sizeof(Node)) ? sizeof(T) : sizeof(Node);
    static constexpr std::size_t block_align_ =
        (alignof(T) > alignof(Node)) ? alignof(T) : alignof(Node);

    void expand() {
        constexpr std::size_t blocks_per_chunk = 64;
        std::size_t chunk_size = block_size_ * blocks_per_chunk;
        chunk_size = (chunk_size + block_align_ - 1) & ~(block_align_ - 1);
        // ...
    }
};

2. 슬랩 할당자 (Slab Allocator)

핵심 아이디어

슬랩 할당자는 여러 크기 클래스(예: 32, 64, 128, 256, 512 바이트)별로 풀을 두고, 요청 크기에 맞는 최소 풀에서 할당합니다. Linux 커널의 슬랩 할당자에서 이름을 따왔습니다.

flowchart LR
    subgraph Request["할당 요청"]
        R1["48바이트"]
        R2["200바이트"]
    end
    subgraph Slabs["슬랩"]
        S32["32B 풀"]
        S64["64B 풀"]
        S128["128B 풀"]
        S256["256B 풀"]
    end
    R1 --> S64
    R2 --> S256

크기 클래스 선택

// 32, 64, 128, 256, 512, 1024, 2048 ...
constexpr std::size_t size_classes[] = {32, 64, 128, 256, 512, 1024, 2048};

std::size_t get_size_class(std::size_t size) {
    for (std::size_t sc : size_classes) {
        if (size <= sc) return sc;
    }
    return 0;  // fallback to heap
}

슬랩 할당자 구현

// slab_allocator.hpp
#include <cstddef>
#include <new>
#include <array>
#include <vector>

class FixedBlockPool {
public:
    explicit FixedBlockPool(std::size_t block_size, std::size_t blocks_per_chunk = 64)
        : block_size_(std::max(block_size, sizeof(void*)))
        , blocks_per_chunk_(blocks_per_chunk)
        , head_(nullptr) {}

    void* allocate() {
        if (!head_) expand();
        if (!head_) return nullptr;
        void* p = head_;
        head_ = *static_cast<void**>(head_);
        return p;
    }

    void deallocate(void* p) {
        if (!p) return;
        *static_cast<void**>(p) = head_;
        head_ = p;
    }

private:
    void expand() {
        std::size_t chunk_size = block_size_ * blocks_per_chunk_;
        std::size_t align = alignof(std::max_align_t);
        chunk_size = (chunk_size + align - 1) & ~(align - 1);

        char* chunk = static_cast<char*>(::operator new(chunk_size));
        for (std::size_t i = 0; i < blocks_per_chunk_; ++i) {
            void* block = chunk + i * block_size_;
            *static_cast<void**>(block) = head_;
            head_ = block;
        }
    }

    std::size_t block_size_;
    std::size_t blocks_per_chunk_;
    void* head_;
};

class SlabAllocator {
public:
    static constexpr std::size_t NUM_CLASSES = 7;
    static constexpr std::size_t SIZE_CLASSES[NUM_CLASSES] =
        {32, 64, 128, 256, 512, 1024, 2048};

    void* allocate(std::size_t size) {
        std::size_t sc = get_size_class(size);
        if (sc == 0) return ::operator new(size);
        return pools_[get_class_index(sc)].allocate();
    }

    void deallocate(void* p, std::size_t size) {
        if (!p) return;
        std::size_t sc = get_size_class(size);
        if (sc == 0) {
            ::operator delete(p);
            return;
        }
        pools_[get_class_index(sc)].deallocate(p);
    }

private:
    static std::size_t get_size_class(std::size_t size) {
        for (std::size_t i = 0; i < NUM_CLASSES; ++i) {
            if (size <= SIZE_CLASSES[i]) return SIZE_CLASSES[i];
        }
        return 0;
    }

    static std::size_t get_class_index(std::size_t sc) {
        for (std::size_t i = 0; i < NUM_CLASSES; ++i) {
            if (SIZE_CLASSES[i] == sc) return i;
        }
        return 0;
    }

    std::array<FixedBlockPool, NUM_CLASSES> pools_{
        FixedBlockPool{32, 64},
        FixedBlockPool{64, 64},
        FixedBlockPool{128, 64},
        FixedBlockPool{256, 64},
        FixedBlockPool{512, 32},
        FixedBlockPool{1024, 16},
        FixedBlockPool{2048, 8},
    };
};

주의: deallocatesize를 넘겨야 올바른 풀에 반환됩니다. malloc/free처럼 size 없이 쓰려면 블록에 크기 메타데이터를 저장하는 방식이 필요합니다.


3. 메모리 아레나 (Memory Arena)

핵심 아이디어

메모리 아레나는 큰 버퍼를 한 번 할당하고, 커서를 앞으로만 이동시키며 순차 할당합니다. 개별 deallocate는 지원하지 않고, reset()으로 커서를 처음으로 되돌려 “전체 해제”합니다.

flowchart LR
    subgraph Arena["메모리 아레나"]
        direction LR
        A1["[할당1][할당2][할당3]     ..."]
        C["커서 →"]
    end
    subgraph Reset["reset()"]
        R["커서 = 0"]
    end
    A1 --> C
    C --> R

아레나 구현

// memory_arena.hpp
#include <cstddef>
#include <new>
#include <vector>
#include <cstring>

class MemoryArena {
public:
    explicit MemoryArena(std::size_t initial_size = 65536)
        : capacity_(initial_size)
        , offset_(0) {
        buffer_ = static_cast<char*>(::operator new(capacity_));
    }

    ~MemoryArena() {
        ::operator delete(buffer_);
    }

    MemoryArena(const MemoryArena&) = delete;
    MemoryArena& operator=(const MemoryArena&) = delete;

    void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
        std::size_t aligned_offset = (offset_ + alignment - 1) & ~(alignment - 1);
        if (aligned_offset + size > capacity_) {
            return nullptr;  // 또는 expand
        }
        void* p = buffer_ + aligned_offset;
        offset_ = aligned_offset + size;
        return p;
    }

    void reset() {
        offset_ = 0;
    }

    std::size_t used() const { return offset_; }
    std::size_t capacity() const { return capacity_; }

private:
    char* buffer_;
    std::size_t capacity_;
    std::size_t offset_;
};

확장 가능 아레나 (청크 체인)

// arena_expandable.hpp
#include <cstddef>
#include <new>
#include <vector>

class ExpandableArena {
public:
    explicit ExpandableArena(std::size_t chunk_size = 65536)
        : chunk_size_(chunk_size)
        , current_offset_(0) {
        chunks_.push_back(static_cast<char*>(::operator new(chunk_size_)));
    }

    ~ExpandableArena() {
        for (char* c : chunks_) ::operator delete(c);
    }

    void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
        std::size_t aligned = (current_offset_ + alignment - 1) & ~(alignment - 1);
        if (aligned + size > chunk_size_) {
            chunks_.push_back(static_cast<char*>(::operator new(chunk_size_)));
            current_offset_ = 0;
            aligned = 0;
        }
        char* chunk = chunks_.back();
        void* p = chunk + aligned;
        current_offset_ = aligned + size;
        return p;
    }

    void reset() {
        current_offset_ = 0;
        // chunks_는 유지, 다음 allocate에서 재사용
    }

private:
    std::size_t chunk_size_;
    std::size_t current_offset_;
    std::vector<char*> chunks_;
};

4. 완전한 고급 메모리 풀 예제

예제 1: 게임 엔티티 객체 풀 (프레임 리셋)

// game_entity_pool.cpp
#include <cstddef>
#include <new>
#include <vector>
#include <iostream>

struct Entity {
    int id;
    float x, y;
    Entity() : id(0), x(0), y(0) {}
    Entity(int i, float x_, float y_) : id(i), x(x_), y(y_) {}
};

template <typename T>
class FrameObjectPool {
public:
    explicit FrameObjectPool(std::size_t block_size, std::size_t capacity = 256)
        : block_size_(block_size)
        , capacity_(capacity)
        , count_(0) {
        storage_ = static_cast<char*>(::operator new(block_size_ * capacity_));
        for (std::size_t i = 0; i < capacity_; ++i) {
            free_list_.push_back(storage_ + i * block_size_);
        }
    }

    ~FrameObjectPool() {
        ::operator delete(storage_);
    }

    template <typename... Args>
    T* acquire(Args&&... args) {
        if (free_list_.empty()) return nullptr;
        void* mem = free_list_.back();
        free_list_.pop_back();
        ++count_;
        return new (mem) T(std::forward<Args>(args)...);
    }

    void release(T* obj) {
        if (!obj) return;
        obj->~T();
        free_list_.push_back(static_cast<void*>(obj));
        --count_;
    }

    void reset() {
        free_list_.clear();
        for (std::size_t i = 0; i < capacity_; ++i) {
            free_list_.push_back(storage_ + i * block_size_);
        }
        count_ = 0;
    }

private:
    std::size_t block_size_;
    std::size_t capacity_;
    std::size_t count_;
    char* storage_;
    std::vector<void*> free_list_;
};

int main() {
    FrameObjectPool<Entity> pool(sizeof(Entity), 128);

    std::vector<Entity*> entities;
    for (int i = 0; i < 10; ++i) {
        entities.push_back(pool.acquire(i, 100.0f + i, 200.0f + i));
    }
    for (auto* e : entities) {
        pool.release(e);
    }
    pool.reset();
    std::cout << "Frame pool test OK\n";
    return 0;
}

예제 2: HTTP 요청 스코프 아레나

// http_request_arena.cpp
#include <memory_resource>
#include <vector>
#include <string>
#include <map>

void handleRequest(const std::string& raw_request) {
    // 요청 스코프: 64KB 아레나
    std::array<std::byte, 65536> stack_buffer;
    std::pmr::monotonic_buffer_resource arena{
        stack_buffer.data(), stack_buffer.size(),
        std::pmr::new_delete_resource()
    };

    std::pmr::vector<std::pmr::string> path_segments(&arena);
    std::pmr::map<std::pmr::string, std::pmr::string, std::less<>>
        headers(&arena);
    std::pmr::string body(&arena);

    // 파싱...
    path_segments.push_back("api");
    path_segments.push_back("v1");
    headers["Content-Type"] = "application/json";
    body = "{\"key\":\"value\"}";

    // 처리...
    // 함수 종료 시 arena 소멸 → 모든 할당 일괄 해제
}

예제 3: 슬랩 + std::allocator 트레이트

// slab_allocator_traits.cpp
template <typename T>
class SlabAllocatorAdapter {
public:
    using value_type = T;
    SlabAllocatorAdapter(SlabAllocator* alloc) : alloc_(alloc) {}
    T* allocate(std::size_t n) {
        return static_cast<T*>(alloc_->allocate(n * sizeof(T)));
    }
    void deallocate(T* p, std::size_t n) {
        alloc_->deallocate(p, n * sizeof(T));
    }
    SlabAllocator* alloc_;
};

// 사용: std::vector<int, SlabAllocatorAdapter<int>> v{&slab};

5. 자주 발생하는 에러와 해결법

에러 1: 풀 수명보다 객체가 오래 살 때 (Use-After-Free)

증상: 크래시, 정의되지 않은 동작, 힙 손상.

원인: 풀이 먼저 소멸되었는데, 그 풀에서 할당받은 객체가 아직 참조될 때.

// ❌ 잘못된 코드
ObjectPool<Bullet>* createPool() {
    ObjectPool<Bullet> pool;
    return &pool;  // 반환 시 pool 소멸!
}
void use() {
    auto* pool = createPool();
    Bullet* b = pool->acquire();  // UB: 이미 소멸된 풀
}

해결법:

// ✅ 올바른 코드: 풀 수명을 객체보다 길게
ObjectPool<Bullet> pool;  // 전역 또는 장수명
void use() {
    Bullet* b = pool.acquire();
    // ...
    pool.release(b);
}

에러 2: 객체 풀에서 release 없이 풀 소멸

증상: 소멸자가 호출되지 않은 객체가 남아 있음. 리소스 누수(파일 핸들, 락 등).

원인: release()를 호출하지 않고 풀이 소멸되면, 풀에 남아 있는 객체의 소멸자가 호출되지 않습니다.

// ❌ 잘못된 코드
{
    ObjectPool<FileHandle> pool;
    FileHandle* f = pool.acquire("test.txt");
    // f 사용...
    // pool.release(f) 누락!
}  // pool 소멸 시 f->~FileHandle() 호출 안 됨 → 파일 미닫힘

해결법:

// ✅ RAII 래퍼 사용
struct PooledHandle {
    ObjectPool<FileHandle>* pool;
    FileHandle* ptr;
    ~PooledHandle() { if (pool && ptr) pool->release(ptr); }
};

에러 3: 슬랩에서 잘못된 크기로 반환

증상: 메모리 손상, 크래시. A 풀에서 할당하고 B 풀에 반환.

원인: deallocate(p, wrong_size)로 다른 크기 클래스 풀에 반환.

// ❌ 잘못된 코드
void* p = slab.allocate(100);  // 128바이트 풀에서 할당
slab.deallocate(p, 64);       // 64바이트 풀에 반환 → UB

해결법:

// ✅ 할당 시 크기 저장 (또는 호출자가 정확히 전달)
struct PooledPtr {
    void* ptr;
    std::size_t size;
};
// deallocate 시 저장된 size 사용

에러 4: 아레나에서 개별 해제 기대

증상: deallocate가 없거나 no-op인데, 개별 해제를 기대함.

원인: 아레나는 reset()으로만 해제합니다.

// ❌ 잘못된 기대
ExpandableArena arena;
void* p1 = arena.allocate(100);
void* p2 = arena.allocate(200);
// p1만 해제하고 싶음 — 불가능

해결법: 개별 해제가 필요하면 또는 슬랩을 사용. 아레나는 “스코프 단위 일괄 해제”용입니다.

에러 5: 정렬 무시

증상: SIMD 타입(__m256) 사용 시 SIGBUS, 크래시.

원인: allocate에서 요청된 alignment를 지키지 않음.

// ❌ 잘못된 구현
void* allocate(std::size_t size) {
    void* p = buffer_ + offset_;
    offset_ += size;
    return p;  // alignment 보장 안 됨
}

해결법:

// ✅ alignment 준수
void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
    std::size_t aligned = (offset_ + alignment - 1) & ~(alignment - 1);
    if (aligned + size > capacity_) return nullptr;
    void* p = buffer_ + aligned;
    offset_ = aligned + size;
    return p;
}

에러 6: 스레드 안전성 오해

증상: 멀티스레드에서 간헐적 크래시.

원인: 객체 풀/슬랩/아레나가 스레드 안전하지 않은데 여러 스레드가 공유.

// ❌ 위험
ObjectPool<Bullet> global_pool;
void worker() {
    Bullet* b = global_pool.acquire();  // data race
    // ...
    global_pool.release(b);
}

해결법:

// ✅ 스레드 로컬 풀
void worker() {
    thread_local ObjectPool<Bullet> pool;
    Bullet* b = pool.acquire();
    // ...
    pool.release(b);
}

6. 성능 벤치마크

벤치마크 1: new/delete vs 객체 풀 vs 슬랩

// benchmark_memory_pools.cpp
// g++ -std=c++17 -O2 -o bench benchmark_memory_pools.cpp
#include <chrono>
#include <iostream>
#include <vector>

struct SmallObject {
    int id;
    float x, y, z;
    SmallObject() : id(0), x(0), y(0), z(0) {}
};

constexpr std::size_t N = 500000;

void bench_new_delete() {
    std::vector<SmallObject*> ptrs;
    ptrs.reserve(N);
    auto start = std::chrono::high_resolution_clock::now();
    for (std::size_t i = 0; i < N; ++i) {
        ptrs.push_back(new SmallObject());
    }
    for (auto* p : ptrs) delete p;
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "new/delete:     " << ms << " ms, "
              << (N * 1000 / (ms + 1)) << " K ops/sec\n";
}

void bench_object_pool() {
    ObjectPool<SmallObject> pool;
    std::vector<SmallObject*> ptrs;
    ptrs.reserve(N);
    auto start = std::chrono::high_resolution_clock::now();
    for (std::size_t i = 0; i < N; ++i) {
        ptrs.push_back(pool.acquire());
    }
    for (auto* p : ptrs) pool.release(p);
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "ObjectPool:     " << ms << " ms, "
              << (N * 1000 / (ms + 1)) << " K ops/sec\n";
}

void bench_slab() {
    SlabAllocator slab;
    std::vector<void*> ptrs;
    ptrs.reserve(N);
    auto start = std::chrono::high_resolution_clock::now();
    for (std::size_t i = 0; i < N; ++i) {
        ptrs.push_back(slab.allocate(sizeof(SmallObject)));
    }
    for (std::size_t i = 0; i < N; ++i) {
        slab.deallocate(ptrs[i], sizeof(SmallObject));
    }
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "SlabAllocator:  " << ms << " ms, "
              << (N * 1000 / (ms + 1)) << " K ops/sec\n";
}

int main() {
    std::cout << "N = " << N << " alloc+free pairs\n";
    bench_new_delete();
    bench_object_pool();
    bench_slab();
    return 0;
}

예상 결과 (참고용)

할당자50만 회 (ms)K ops/sec상대
new/delete~80~62501x
ObjectPool~12~41666~6.7x
SlabAllocator~15~33333~5.3x

벤치마크 2: 아레나 순차 할당

void bench_arena_sequential() {
    MemoryArena arena(1024 * 1024);
    for (int round = 0; round < 10; ++round) {
        arena.reset();
        for (std::size_t i = 0; i < 100000; ++i) arena.allocate(32);
    }
}

성능 비교 다이어그램

flowchart TB
    subgraph Slow["느린 경로 (new/delete)"]
        S1[malloc 락]
        S2[힙 탐색]
        S3[메타데이터]
        S4[생성자]
        S5[소멸자]
    end
    subgraph Fast["빠른 경로 (객체 풀)"]
        F1[포인터 pop]
        F2[placement new]
        F3[소멸자]
        F4[포인터 push]
    end
    S1 --> S2 --> S3 --> S4
    F1 --> F2
    F3 --> F4

7. 프로덕션 패턴

패턴 1: 게임 프레임 풀 (객체 풀 + reset)

class GameFramePool {
public:
    void beginFrame() {
        bullet_pool_.reset();
        particle_pool_.reset();
    }

    Bullet* createBullet(float x, float y, float vx, float vy) {
        return bullet_pool_.acquire(x, y, vx, vy);
    }

    void destroyBullet(Bullet* b) {
        bullet_pool_.release(b);
    }

    void endFrame() {
        // 모든 프레임 내 생성 객체는 반드시 destroy 호출
        // 또는 reset 시 남은 객체 정리
    }

private:
    FrameObjectPool<Bullet> bullet_pool_{sizeof(Bullet), 1024};
    FrameObjectPool<Particle> particle_pool_{sizeof(Particle), 4096};
};

패턴 2: HTTP 요청 스코프 (아레나)

void handleHttpRequest(const Request& req) {
    std::pmr::monotonic_buffer_resource arena(
        std::pmr::new_delete_resource());
    std::pmr::vector<std::pmr::string> path(&arena);
    std::pmr::map<std::pmr::string, std::pmr::string, std::less<>>
        headers(&arena);

    parseRequest(req, path, headers);
    Response resp = process(path, headers);
    sendResponse(resp);
}  // arena 소멸 → 일괄 해제

패턴 3: 크기별 슬랩 + 모니터링

class MonitoredSlabAllocator : public SlabAllocator {
public:
    void* allocate(std::size_t size) override {
        void* p = SlabAllocator::allocate(size);
        if (p) {
            ++alloc_count_;
            current_used_ += get_size_class(size);
            peak_used_ = std::max(peak_used_, current_used_);
        }
        return p;
    }

    void deallocate(void* p, std::size_t size) override {
        SlabAllocator::deallocate(p, size);
        current_used_ -= get_size_class(size);
    }

    std::size_t peak_usage() const { return peak_used_; }

private:
    std::size_t alloc_count_ = 0;
    std::size_t current_used_ = 0;
    std::size_t peak_used_ = 0;
};

패턴 4: 스레드 로컬 풀 + 상위 풀 폴백

class TieredPool {
public:
    void* allocate(std::size_t size) {
        auto& local = get_local_pool();
        void* p = local.allocate(size);
        if (!p) {
            std::lock_guard lock(mutex_);
            p = global_pool_.allocate(size);
        }
        return p;
    }

private:
    SlabAllocator& get_local_pool() {
        thread_local SlabAllocator local;
        return local;
    }
    std::mutex mutex_;
    SlabAllocator global_pool_;
};

패턴 5: 구현 체크리스트

- [ ] 풀/아레나 수명이 할당 객체 수명보다 길다
- [ ] 객체 풀 사용 시 release() 누락 방지 (RAII 래퍼)
- [ ] 슬랩 deallocate 시 올바른 size 전달
- [ ] 아레나는 개별 해제 불가 — 스코프 단위 사용
- [ ] 멀티스레드 시 스레드 로컬 풀 또는 락
- [ ] 프로파일링으로 실제 이득 확인 후 적용

8. 정리

패턴용도장점한계
객체 풀타입별 대량 생성/해제생성/소멸자 비용까지 절감타입별 풀 필요
슬랩 할당자크기 다양할 때내부 단편화 감소deallocate 시 size 필요
메모리 아레나순차 할당, 일괄 해제구현 단순, 오버헤드 최소개별 해제 불가

핵심 원칙:

  1. 풀 수명 > 객체 수명
  2. 객체 풀release() 반드시 호출
  3. 슬랩deallocate(p, size)에 정확한 size 전달
  4. 아레나는 스코프 단위로만 사용
  5. 프로파일링으로 병목 확인 후 적용

자주 묻는 질문 (FAQ)

Q. 객체 풀 vs std::pmr::pool_resource 차이는?

A. 객체 풀은 타입 전용이고 acquire/release로 생성/소멸을 풀과 연동합니다. pool_resource는 범용 메모리 풀로, 컨테이너에 주입해 씁니다. 타입별 생성 비용까지 줄이려면 객체 풀이 유리합니다.

Q. 슬랩 크기 클래스를 어떻게 정하나요?

A. 실제 할당 크기 분포를 프로파일링해, 90% 이상이 커버되는 클래스를 선택합니다. 32, 64, 128, 256, 512, 1024 등 2의 거듭제곱이 일반적입니다.

Q. 아레나와 monotonic_buffer_resource 차이는?

A. 개념적으로 동일합니다. std::pmr::monotonic_buffer_resource가 표준 구현이고, 커스텀 아레나는 프로젝트별 확장(로깅, 통계 등)이 필요할 때 직접 구현합니다.

Q. 선행으로 읽으면 좋은 글은?

A. 메모리 풀 #48-3, std::pmr #39-2를 먼저 읽으면 좋습니다.

Q. 더 깊이 공부하려면?

A. cppreferencestd::memory_resource, jemalloc 슬랩 설계, Linux 커널 슬랩 할당자 문서를 참고하세요.

한 줄 요약: 객체 풀·슬랩·아레나로 고급 메모리 관리를 적용할 수 있습니다.

이전 글: C++ SIMD 최적화 실전 #51-2

다음 글: C++ 컴파일 타임 최적화 #51-5


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3