C++ 메모리 관리 완벽 가이드 | 할당자·풀·아레나·프로덕션 패턴 [#55-5]

C++ 메모리 관리 완벽 가이드 | 할당자·풀·아레나·프로덕션 패턴 [#55-5]

이 글의 핵심

C++ malloc 병목, 힙 단편화, 메모리 누수 해결. 커스텀 할당자, 메모리 풀, 아레나 설계·구현, 흔한 에러, 성능 팁, 프로덕션 패턴까지 실전 코드로 다룹니다. C++에서 new/delete는 편리하지만, 대량 할당/해제가 반복되면 malloc 오버헤드, 힙 단편화, 락 경합이 누적됩니다. 게임 엔진에서 매 프레임 수천 개의 Bullet, Particle을 생성·해제하면 60fps를 달성하지 못하고,.

들어가며: malloc이 30%를 차지할 때

”프로파일러에서 malloc이 상위를 차지해요”

C++에서 new/delete는 편리하지만, 대량 할당/해제가 반복되면 malloc 오버헤드, 힙 단편화, 락 경합이 누적됩니다. 게임 엔진에서 매 프레임 수천 개의 Bullet, Particle을 생성·해제하면 60fps를 달성하지 못하고, HTTP 서버에서 요청마다 파싱 결과를 할당하면 24시간 운영 후 OOM이 발생합니다.

비유하면 “창고에서 상자 하나씩 꺼내 쓰는 것”이 new/delete라면, 메모리 풀은 “미리 큰 공간을 잡아두고 그 안에서 나눠 쓰는 것”, 아레나는 “한 번에 큰 공간을 잡고 순차적으로 잘라 쓰는 것”입니다.

flowchart LR
  subgraph problem[문제 상황]
    P1[malloc 병목]
    P2[힙 단편화]
    P3[락 경합]
    P4[OOM]
  end

  subgraph solution[해결 방향]
    S1[커스텀 할당자]
    S2[메모리 풀]
    S3[메모리 아레나]
    S4[std::pmr]
  end

  P1 --> S1
  P2 --> S2
  P3 --> S2
  P4 --> S3

이 글을 읽으면:

  • 커스텀 할당자, 메모리 풀, 아레나를 실전 코드로 구현할 수 있습니다.
  • 자주 발생하는 에러와 해결법을 알 수 있습니다.
  • 성능 최적화 팁과 프로덕션 패턴을 활용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

추가 문제 시나리오

시나리오 1: 게임 프레임 드랍
매 프레임 Bullet 객체를 수천 개 생성·해제하는데, 프로파일러에서 operator new가 30% 이상을 차지합니다. 60fps 목표를 달성하지 못합니다.

시나리오 2: 장시간 실행 후 OOM
HTTP 서버가 24시간 이상 운영 후 malloc이 실패합니다. 전체 메모리는 여유가 있는데 할당이 실패합니다. 힙 단편화 때문입니다.

시나리오 3: 멀티스레드 확장성 부족
16코어 서버에서 16스레드로 요청을 처리하는데, throughput이 4스레드의 2배 정도에 그칩니다. 전역 malloc의 락 경합 때문입니다.

시나리오 4: HTTP 요청 처리 중 할당 폭발
요청마다 path_segments, headers, body를 할당하는데, 개별 해제가 필요 없고 요청 끝에 일괄 해제하면 됩니다. free list 관리 오버헤드가 아깝습니다.

시나리오 5: 캐시 미스로 순회 느림
링크드 리스트 노드를 new로 할당해 사용하는데, 순회 시 캐시 미스가 많아 전체 루프가 느립니다.

시나리오특징권장 패턴
malloc 병목할당 횟수 과다메모리 풀, 객체 풀
힙 단편화장시간 OOM풀, 아레나
스레드 경합멀티스레드 확장성스레드 로컬 풀
순차 할당, 일괄 해제HTTP 요청, 프레임메모리 아레나
캐시 효율순회 성능연속 풀 블록

목차

  1. 커스텀 할당자 (Allocator)
  2. 메모리 풀 (Memory Pool)
  3. 메모리 아레나 (Memory Arena)
  4. std::pmr 통합
  5. 완전한 메모리 관리 예제
  6. 자주 발생하는 에러와 해결법
  7. 성능 최적화 팁
  8. 프로덕션 패턴
  9. 정리

1. 커스텀 할당자 (Allocator)

C++ 표준 할당자 요구사항

STL 컨테이너는 Allocator 템플릿 파라미터를 통해 메모리 소스를 지정합니다. 할당자는 allocate, deallocate, construct, destroy 등을 제공해야 합니다.

flowchart TB
  subgraph Container["std::vector"]
    V[데이터]
  end
  subgraph Allocator[커스텀 할당자]
    A1[allocate]
    A2[deallocate]
  end
  V --> A1
  A1 --> A2

최소 할당자 구현

// custom_allocator.hpp
// g++ -std=c++17 -O2 -o alloc_test custom_allocator_example.cpp
#include <memory>
#include <cstddef>
#include <new>

template <typename T>
class SimpleAllocator {
public:
    using value_type = T;

    SimpleAllocator() = default;

    template <typename U>
    SimpleAllocator(const SimpleAllocator<U>&) noexcept {}

    T* allocate(std::size_t n) {
        if (n == 0) return nullptr;
        if (n > static_cast<std::size_t>(-1) / sizeof(T)) {
            throw std::bad_alloc();
        }
        void* p = ::operator new(n * sizeof(T));
        return static_cast<T*>(p);
    }

    void deallocate(T* p, std::size_t n) noexcept {
        (void)n;
        ::operator delete(p);
    }

    template <typename U, typename... Args>
    void construct(U* p, Args&&... args) {
        new (p) U(std::forward<Args>(args)...);
    }

    template <typename U>
    void destroy(U* p) {
        p->~U();
    }
};

template <typename T, typename U>
bool operator==(const SimpleAllocator<T>&, const SimpleAllocator<U>&) noexcept {
    return true;
}

template <typename T, typename U>
bool operator!=(const SimpleAllocator<T>&, const SimpleAllocator<U>&) noexcept {
    return false;
}

사용 예: vector에 할당자 주입

#include "custom_allocator.hpp"
#include <vector>
#include <iostream>

int main() {
    std::vector<int, SimpleAllocator<int>> v;
    v.reserve(100);
    for (int i = 0; i < 100; ++i) {
        v.push_back(i);
    }
    std::cout << "v.size() = " << v.size() << "\n";
    return 0;
}

2. 메모리 풀 (Memory Pool)

고정 블록 풀 설계

핵심 아이디어: 한 번에 큰 메모리를 할당하고, 고정 크기 블록으로 나눠 free list로 관리합니다.

flowchart TB
  subgraph Heap[전역 힙]
    H[한 번 큰 블록 할당]
  end
  subgraph Pool[메모리 풀]
    B[청크]
    B --> S1[블록 1]
    B --> S2[블록 2]
    B --> S3[블록 N]
  end
  subgraph FreeList[Free List]
    FL[head → 블록1 → 블록2 → ...]
  end
  H --> B
  S1 -.->|사용 가능| FL
  S2 -.->|사용 가능| FL
  S3 -.->|할당됨| A1[객체]

완전한 고정 블록 풀 구현

// fixed_block_pool.hpp
#include <cstddef>
#include <new>
#include <algorithm>

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

    ~FixedBlockPool() {
        while (head_) {
            Node* next = head_->next;
            ::operator delete(head_);
            head_ = next;
        }
    }

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

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

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

private:
    union Node {
        Node* next;
    };

    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) {
            Node* node = reinterpret_cast<Node*>(chunk + i * block_size_);
            node->next = head_;
            head_ = node;
        }
    }

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

placement new와 풀 연동

// pool_new_delete.hpp
#include "fixed_block_pool.hpp"
#include <utility>

template <typename T>
T* pool_new(FixedBlockPool& pool) {
    void* p = pool.allocate();
    return new (p) T();
}

template <typename T, typename... Args>
T* pool_new(FixedBlockPool& pool, Args&&... args) {
    void* p = pool.allocate();
    return new (p) T(std::forward<Args>(args)...);
}

template <typename T>
void pool_delete(FixedBlockPool& pool, T* obj) {
    if (obj) {
        obj->~T();
        pool.deallocate(obj);
    }
}

// 사용 예
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() {
    FixedBlockPool pool(sizeof(Bullet), 128);
    Bullet* b = pool_new<Bullet>(pool, 100.0f, 200.0f, 5.0f, 0.0f);
    b->x = 50.0f;
    pool_delete(pool, b);
    return 0;
}

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>

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

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

    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 =
            (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; }

    std::size_t used() const { return current_offset_; }

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

HTTP 요청 스코프 아레나 예제

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

void handleRequest(const std::string& raw_request) {
    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 소멸 → 모든 할당 일괄 해제
}

4. std::pmr 통합

Polymorphic Memory Resource

C++17의 std::pmr다형 메모리 리소스를 제공합니다. memory_resource를 상속해 do_allocate, do_deallocate를 구현하면 STL 컨테이너에 주입할 수 있습니다.

// pool_memory_resource.hpp
#include <memory_resource>
#include <cstddef>
#include <new>

class PoolMemoryResource : public std::pmr::memory_resource {
public:
    explicit PoolMemoryResource(std::size_t block_size = 256,
                                std::size_t blocks_per_chunk = 64)
        : block_size_(block_size), pool_(block_size, blocks_per_chunk) {}

protected:
    void* do_allocate(std::size_t bytes, std::size_t alignment) override {
        if (bytes > block_size_) {
            return ::operator new(bytes);
        }
        return pool_.allocate();
    }

    void do_deallocate(void* p, std::size_t bytes,
                       std::size_t alignment) override {
        if (bytes > block_size_) {
            ::operator delete(p);
            return;
        }
        pool_.deallocate(p);
    }

    bool do_is_equal(const memory_resource& other) const noexcept override {
        return this == &other;
    }

private:
    std::size_t block_size_;
    FixedBlockPool pool_;
};

// 사용
int main() {
    PoolMemoryResource pool_res(256, 64);
    std::pmr::vector<int> v(&pool_res);
    v.resize(1000);
    return 0;
}

monotonic_buffer_resource

#include <memory_resource>
#include <vector>
#include <array>

void frameAllocation() {
    std::array<std::byte, 1024 * 1024> buffer;
    std::pmr::monotonic_buffer_resource arena{
        buffer.data(), buffer.size(),
        std::pmr::new_delete_resource()};

    std::pmr::vector<int> temp_data(&arena);
    temp_data.resize(10000);

    // 프레임 처리...
}  // arena 소멸 → 일괄 해제

5. 완전한 메모리 관리 예제

예제 1: 슬랩 할당자 (크기별 풀)

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

class FixedBlockPool {
    // (위 FixedBlockPool 구현과 동일)
};

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},
    };
};

예제 2: 객체 풀 (Object Pool)

// object_pool.hpp
#include <cstddef>
#include <new>
#include <utility>

template <typename T>
class ObjectPool {
public:
    ObjectPool() = default;

    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*)");
    Node* head_ = nullptr;
};

예제 3: 스레드 로컬 풀

// thread_local_pool.cpp
#include <thread>
#include <vector>
#include <iostream>

class ThreadLocalPool {
public:
    static FixedBlockPool& instance() {
        thread_local FixedBlockPool pool(256, 64);
        return pool;
    }
};

struct Entity {
    int id;
    float x, y;
};

int main() {
    std::vector<std::thread> threads;
    for (int t = 0; t < 4; ++t) {
        threads.emplace_back([t]() {
            auto& pool = ThreadLocalPool::instance();
            for (int i = 0; i < 1000; ++i) {
                Entity* e = static_cast<Entity*>(pool.allocate());
                e->id = t * 1000 + i;
                e->x = e->y = 0.0f;
                pool.deallocate(e);
            }
        });
    }
    for (auto& th : threads) th.join();
    std::cout << "스레드 로컬 풀 테스트 완료\n";
    return 0;
}

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

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

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

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

// ❌ 잘못된 코드
void* create() {
    FixedBlockPool pool(256);
    return pool.allocate();
}
void use() {
    void* p = create();  // pool은 함수 종료 시 소멸!
    *(int*)p = 42;       // UB: 이미 해제된 풀의 메모리
}

해결법:

// ✅ 올바른 코드: 풀 수명을 객체보다 길게
FixedBlockPool pool(256);  // 전역 또는 장수명
void* create() { return pool.allocate(); }
void use() {
    void* p = create();
    *(int*)p = 42;
    pool.deallocate(p);
}

에러 2: 잘못된 풀에 반환

증상: 메모리 손상, 크래시.

원인: 풀 A에서 할당받고 풀 B에 반환함.

// ❌ 잘못된 코드
FixedBlockPool pool1(256), pool2(256);
void* p = pool1.allocate();
pool2.deallocate(p);  // 다른 풀에 반환 → UB

해결법:

// ✅ 할당한 풀에 반환
void* p = pool1.allocate();
pool1.deallocate(p);

에러 3: 블록 크기보다 큰 객체 할당

증상: 인접 블록 덮어쓰기, 메모리 손상.

원인: sizeof(Entity)가 블록 크기(256)보다 큰데 풀에서 할당함.

// ❌ 잘못된 코드
FixedBlockPool pool(256);
struct LargeEntity { char data[512]; };
LargeEntity* e = static_cast<LargeEntity*>(pool.allocate());  // 오버플로!

해결법:

// ✅ fallback: 블록 크기 초과 시 전역 힙
void* allocate(std::size_t size) {
    if (size > block_size_) return ::operator new(size);
    return allocate();
}

에러 4: 이중 해제 (Double Free)

증상: 크래시, free list 손상.

// ❌ 잘못된 코드
void* p = pool.allocate();
pool.deallocate(p);
pool.deallocate(p);  // double free

해결법:

// ✅ 해제 후 nullptr
pool.deallocate(p);
p = nullptr;

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

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

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

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

에러 6: 정렬 무시

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

해결법:

// ✅ 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;
}

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

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

원인: 전역 풀을 여러 스레드가 락 없이 사용함.

해결법:

// ✅ 스레드 로컬 풀
void worker() {
    thread_local FixedBlockPool pool(256);
    void* p = pool.allocate();
    pool.deallocate(p);
}

7. 성능 최적화 팁

팁 1: 프로파일링 우선

원칙: malloc이 실제로 병목인지 확인한 뒤 적용합니다. 추측으로 풀을 도입하면 복잡도만 늘어납니다.

# perf로 malloc 비중 확인 (Linux)
perf record -g ./my_program
perf report

팁 2: 블록 크기 선택

실제 할당 크기 분포를 프로파일링해, 90% 이상이 커버되는 크기를 선택합니다.

32, 64, 128, 256, 512, 1024 (2의 거듭제곱)

팁 3: 스레드 로컬 풀

멀티스레드 시 thread_local 풀을 두면 락 없이 처리량을 높일 수 있습니다.

팁 4: 아레나로 순차 할당

“할당만 하고 끝에 일괄 해제” 패턴에는 아레나가 최적입니다. free list 오버헤드가 없습니다.

팁 5: 벤치마크 결과 (참고용)

할당자50만 회 (ms)K ops/sec상대
new/delete~80~62501x
ObjectPool~12~41666~6.7x
SlabAllocator~15~33333~5.3x
Arena (순차)~8~62500~10x
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

8. 프로덕션 패턴

패턴 1: 게임 프레임 풀

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); }

private:
    ObjectPool<Bullet> bullet_pool_;
    ObjectPool<Particle> particle_pool_;
};

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

void handle_request(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 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:
    FixedBlockPool& get_local_pool() {
        thread_local FixedBlockPool local(256, 64);
        return local;
    }
    std::mutex mutex_;
    FixedBlockPool global_pool_{256, 256};
};

패턴 4: 모니터링 풀

class MonitoredPool {
public:
    void* allocate() {
        void* p = pool_.allocate();
        ++alloc_count_;
        ++current_used_;
        peak_used_ = std::max(peak_used_, current_used_);
        return p;
    }

    void deallocate(void* p) {
        pool_.deallocate(p);
        --current_used_;
    }

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

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

패턴 5: RAII 풀 래퍼

template <typename T>
struct PooledPtr {
    ObjectPool<T>* pool;
    T* ptr;

    ~PooledPtr() {
        if (pool && ptr) pool->release(ptr);
    }

    T* get() { return ptr; }
    T* operator->() { return ptr; }
};

프로덕션 체크리스트

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

9. 정리

패턴용도장점한계
커스텀 할당자STL 컨테이너 주입표준 인터페이스구현 복잡도
메모리 풀고정 크기 대량 할당빠름, 단편화 감소블록 크기 고정
슬랩 할당자크기 다양할 때내부 단편화 감소deallocate 시 size 필요
메모리 아레나순차 할당, 일괄 해제구현 단순, 오버헤드 최소개별 해제 불가
std::pmr표준 통합STL 호환C++17 이상

핵심 원칙:

  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, 메모리 기초 #6-1를 먼저 읽으면 좋습니다.

한 줄 요약: malloc 병목·힙 단편화 해결을 위해 커스텀 할당자·풀·아레나를 적용할 수 있습니다.


참고 자료


관련 글

  • C++ 스택 vs 힙 완벽 가이드 | 재귀 크래시, 메모리 레이아웃, RAII·스마트 포인터 실전 패턴
  • C++ Custom Allocator |
  • C++ Allocator |
  • C++ 메모리 풀 고급 기법 | 객체 풀·슬랩 할당자·메모리 아레나 완벽 가이드 [#51-4]
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3