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 요청, 프레임 | 메모리 아레나 |
| 캐시 효율 | 순회 성능 | 연속 풀 블록 |
목차
- 커스텀 할당자 (Allocator)
- 메모리 풀 (Memory Pool)
- 메모리 아레나 (Memory Arena)
- std::pmr 통합
- 완전한 메모리 관리 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화 팁
- 프로덕션 패턴
- 정리
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 | ~6250 | 1x |
| 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 이상 |
핵심 원칙:
- 풀 수명 > 객체 수명
- 객체 풀은
release()반드시 호출 - 슬랩은
deallocate(p, size)에 정확한 size 전달 - 아레나는 스코프 단위로만 사용
- 프로파일링으로 병목 확인 후 적용
자주 묻는 질문 (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 병목·힙 단편화 해결을 위해 커스텀 할당자·풀·아레나를 적용할 수 있습니다.
참고 자료
- cppreference - std::allocator
- cppreference - std::pmr::memory_resource
- jemalloc — 고성능 할당자
- tcmalloc — 스레드 캐시 malloc
관련 글
- C++ 스택 vs 힙 완벽 가이드 | 재귀 크래시, 메모리 레이아웃, RAII·스마트 포인터 실전 패턴
- C++ Custom Allocator |
- C++ Allocator |
- C++ 메모리 풀 고급 기법 | 객체 풀·슬랩 할당자·메모리 아레나 완벽 가이드 [#51-4]