Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
#한 줄 요약
“Pool은 같은 크기 블록의 묶음에 free list를 더한 구조입니다.” O(1) 할당과 해제, fragmentation 0, deterministic이라는 세 가지를 동시에 제공합니다.
#어떤 문제를 푸는가
Part 3-02에서 간단한 pool을 보았습니다. 이 글은 실용적인 pool 구현을 다룹니다.
요구사항은 이렇습니다.
- O(1) 할당과 해제 — 어떤 상태에서도 일정 시간이 걸려야 합니다.
- Fragmentation 0 — 같은 크기 블록만 다룹니다.
- Thread-safe (선택) — RTOS 환경에서 필요합니다.
- Statistics — 사용량을 추적할 수 있어야 합니다.
ObjectPool<Order, 32> pool;
Order* o1 = pool.allocate(); // O(1)Order* o2 = pool.allocate();pool.deallocate(o1); // O(1)
assert(pool.allocated() == 1);assert(pool.available() == 31);#Pool의 핵심 — Free List
free 블록들을 연결 리스트로 관리합니다. 할당된 블록의 메모리에는 next pointer를 저장해 둡니다.
처음 상태:storage: [block0][block1][block2][block3]free_list_head → block0 → block1 → block2 → block3 → nullptr
block0 할당 후:free_list_head → block1 → block2 → block3 → nullptr반환된 block0의 메모리는 사용자가 사용
block0 해제 후:free_list_head → block0 → block1 → block2 → block3 → nullptr(또는 LIFO: block0 → block1 → block2 → ...)포인터 하나만 보유하면 O(1) push/pop이 가능합니다.
#기본 구현
template<typename T, size_t N>class ObjectPool { // 각 슬롯 — T 또는 next pointer union Slot { alignas(T) std::byte storage[sizeof(T)]; Slot* next; };
Slot slots_[N]; Slot* free_head_; size_t allocated_ = 0;
public: ObjectPool() { // 초기 free list 구성 for (size_t i = 0; i < N - 1; ++i) { slots_[i].next = &slots_[i + 1]; } slots_[N - 1].next = nullptr; free_head_ = &slots_[0]; }
T* allocate() noexcept { if (!free_head_) return nullptr; // 고갈
Slot* slot = free_head_; free_head_ = slot->next; ++allocated_; return reinterpret_cast<T*>(&slot->storage); }
void deallocate(T* p) noexcept { if (!p) return;
Slot* slot = reinterpret_cast<Slot*>(p); slot->next = free_head_; free_head_ = slot; --allocated_; }
// 객체 생성 + 할당 template<typename... Args> T* construct(Args&&... args) noexcept { T* p = allocate(); if (p) new (p) T(std::forward<Args>(args)...); return p; }
// 소멸 + 해제 void destroy(T* p) noexcept { if (p) { p->~T(); deallocate(p); } }
size_t allocated() const { return allocated_; } size_t available() const { return N - allocated_; } bool empty() const { return allocated_ == 0; } bool full() const { return allocated_ == N; }};핵심 아이디어는 union을 활용해 같은 메모리를 객체 또는 next pointer로 양분한다는 점입니다. 추가 메모리가 전혀 들지 않습니다.
#사용 예 — Order Pool
struct Order { int id; int quantity; float price; Order(int i, int q, float p) : id(i), quantity(q), price(p) {}};
ObjectPool<Order, 32> order_pool;
void process_request(int id, int qty, float price) { auto* order = order_pool.construct(id, qty, price); if (!order) { log_error("order pool full"); return; }
// 처리 process(order);
order_pool.destroy(order);}construct와 destroy로 RAII와 자연스럽게 어울리며, 실패 시 nullptr를 반환합니다.
#RAII wrapper — Pool unique_ptr
template<typename T, size_t N>class PoolUniquePtr { ObjectPool<T, N>* pool_; T* ptr_;
public: PoolUniquePtr(ObjectPool<T, N>* pool, T* ptr) : pool_(pool), ptr_(ptr) {}
~PoolUniquePtr() { if (ptr_) pool_->destroy(ptr_); }
PoolUniquePtr(PoolUniquePtr&& other) noexcept : pool_(other.pool_), ptr_(other.ptr_) { other.ptr_ = nullptr; }
PoolUniquePtr& operator=(PoolUniquePtr&& other) noexcept { if (this != &other) { if (ptr_) pool_->destroy(ptr_); pool_ = other.pool_; ptr_ = other.ptr_; other.ptr_ = nullptr; } return *this; }
PoolUniquePtr(const PoolUniquePtr&) = delete; PoolUniquePtr& operator=(const PoolUniquePtr&) = delete;
T* get() const { return ptr_; } T* operator->() const { return ptr_; } T& operator*() const { return *ptr_; } explicit operator bool() const { return ptr_ != nullptr; }};
template<typename T, size_t N, typename... Args>PoolUniquePtr<T, N> make_pool_unique(ObjectPool<T, N>& pool, Args&&... args) { return PoolUniquePtr<T, N>(&pool, pool.construct(std::forward<Args>(args)...));}사용 예는 다음과 같습니다.
ObjectPool<Order, 32> pool;
void process() { auto order = make_pool_unique(pool, 1, 100, 9.99f); if (!order) return; // pool full
order->process(); // 자동 destroy}std::unique_ptr처럼 예외 안전과 자동 해제를 모두 챙길 수 있습니다.
#Thread-safe pool
RTOS multi-task에서 동시에 할당과 해제가 일어나면 race condition이 생깁니다. mutex나 atomic으로 보호합니다.
template<typename T, size_t N>class ThreadSafePool { Slot slots_[N]; Slot* free_head_; SemaphoreHandle_t mutex_; // FreeRTOS 예
public: ThreadSafePool() { // 초기화 mutex_ = xSemaphoreCreateMutex(); }
T* allocate() noexcept { xSemaphoreTake(mutex_, portMAX_DELAY); T* result = nullptr; if (free_head_) { Slot* slot = free_head_; free_head_ = slot->next; result = reinterpret_cast<T*>(&slot->storage); } xSemaphoreGive(mutex_); return result; }
void deallocate(T* p) noexcept { if (!p) return; xSemaphoreTake(mutex_, portMAX_DELAY); Slot* slot = reinterpret_cast<Slot*>(p); slot->next = free_head_; free_head_ = slot; xSemaphoreGive(mutex_); }};mutex 비용은 일반적인 RTOS에서 1~2 μs 수준입니다. critical section이 짧기 때문에 보통 수용 가능합니다.
#Lock-free pool — atomic free list
더 빠른 옵션입니다. CAS(Compare-And-Swap)로 lock 없이 처리합니다.
template<typename T, size_t N>class LockFreePool { Slot slots_[N]; std::atomic<Slot*> free_head_;
public: T* allocate() noexcept { Slot* head = free_head_.load(std::memory_order_acquire); while (head) { Slot* next = head->next; if (free_head_.compare_exchange_weak( head, next, std::memory_order_release, std::memory_order_acquire)) { return reinterpret_cast<T*>(&head->storage); } } return nullptr; }
void deallocate(T* p) noexcept { if (!p) return; Slot* slot = reinterpret_cast<Slot*>(p); Slot* head = free_head_.load(std::memory_order_relaxed); do { slot->next = head; } while (!free_head_.compare_exchange_weak( head, slot, std::memory_order_release, std::memory_order_relaxed)); }};ABA problem을 주의해야 합니다. 다른 thread가 같은 주소를 재할당하면 CAS가 잘못 성공합니다. tagged pointer나 hazard pointer로 해결합니다.
대부분의 임베디드에서는 mutex 기반이면 충분합니다. lock-free는 극한 환경에서만 고려합니다.
자세한 lock-free는 Part 4-03에서 다룹니다.
#통계와 디버깅
production debugging을 위한 추가 정보입니다.
template<typename T, size_t N>class InstrumentedPool { // ... 기본 pool
size_t peak_allocated_ = 0; size_t total_allocations_ = 0; size_t failed_allocations_ = 0;
public: T* allocate() noexcept { ++total_allocations_; T* p = base_allocate(); if (!p) { ++failed_allocations_; } else { if (allocated_ > peak_allocated_) peak_allocated_ = allocated_; } return p; }
struct Stats { size_t allocated; size_t available; size_t peak; size_t total_allocs; size_t failed; };
Stats get_stats() const { return {allocated_, N - allocated_, peak_allocated_, total_allocations_, failed_allocations_}; }};런타임에 pool 사용 패턴을 추적합니다. 피크가 N에 도달한다면 N을 늘릴지 판단합니다.
#Heap-backed pool — 동적 pool
처음에 malloc을 한 번만 호출하고 이후로는 free list로 관리합니다.
template<typename T>class HeapPool { void* storage_; Slot* free_head_; size_t capacity_;
public: explicit HeapPool(size_t n) : capacity_(n) { storage_ = std::malloc(sizeof(Slot) * n); // ... free list 초기화 }
~HeapPool() { std::free(storage_); }};시작 시점에 한 번만 malloc을 호출하고 이후로는 fragmentation이 없습니다. 시스템 init 단계에서 만들어 두면 런타임 malloc이 사라집니다.
대부분의 임베디드에서는 static pool이 더 안전합니다. heap pool은 시작 시점에야 크기가 결정되는 경우에만 씁니다.
#자주 보는 함정과 안티패턴
#1. Double free
auto* p = pool.allocate();pool.deallocate(p);pool.deallocate(p); // free list 순환 → 무한 루프 또는 corruptionRAII로 감싸 직접 호출을 피합니다.
#2. Use after free
auto* p = pool.allocate();pool.deallocate(p);p->method(); // p의 메모리는 next pointer로 변경됨RAII로 lifetime을 명확히 합니다.
#3. 큰 N으로 .bss 폭증
ObjectPool<HugeStruct, 10000> pool; // sizeof(HugeStruct) * 10000N은 측정을 바탕으로 결정합니다. peak_allocated를 추적한 뒤 10~20% 마진을 둡니다.
#4. Thread safety 가정
single-thread pool을 RTOS task끼리 공유하면 race가 발생합니다. mutex를 두거나 task별로 별도 pool을 씁니다.
#5. Pool 종류 너무 많음
각 타입마다 전용 pool을 두면 메모리도 낭비되고 관리도 복잡해집니다. 비슷한 크기는 공용 pool로 합칩니다.
#6. initial free list 잘못
init 시점에 next pointer가 누락되면 null deref가 발생합니다. 모든 slot을 연결합니다.
#Variable-size — Multi-pool
여러 크기를 처리하려면 여러 pool을 두고 크기에 따라 분기합니다.
class MultiPool { ObjectPool<std::byte, 16> small_pool_; // 16 byte 블록 ObjectPool<std::byte, 64> medium_pool_; // 64 byte ObjectPool<std::byte, 256> large_pool_; // 256 byte
public: void* allocate(size_t n) { if (n <= 16) return small_pool_.allocate(); if (n <= 64) return medium_pool_.allocate(); if (n <= 256) return large_pool_.allocate(); return nullptr; }};크기를 추적해야 하므로 pool마다 별도의 deallocate가 필요합니다.
void deallocate(void* p, size_t n) { if (n <= 16) small_pool_.deallocate(static_cast<std::byte*>(p)); else if (n <= 64) medium_pool_.deallocate(static_cast<std::byte*>(p)); // ...}malloc과 비슷한 generic allocator가 됩니다. 각 pool 내부에는 fragmentation이 없지만, pool 간 균형이 안 맞으면 일부 fragmentation이 발생할 수 있습니다.
#측정 — pool vs malloc
같은 패턴(10000회 alloc/free)을 STM32F4에서 측정합니다.
malloc/free (newlib-nano): alloc: 평균 ~120 cycles, max 800+ (worst case) free: 평균 ~80 cycles fragmentation: 시간에 따라 증가 determinism: 낮음
ObjectPool<Order, 100>: alloc: 12 cycles (constant) free: 10 cycles (constant) fragmentation: 0 determinism: 완벽Pool이 약 10배 빠르고 결정적입니다. real-time critical 환경에서는 사실상 필수입니다.
#정리
- Pool은 고정 크기 블록과 free list로 구성되며 O(1)에 할당과 해제가 가능합니다.
union트릭으로 추가 메모리 없이 free list를 둘 수 있습니다.- Thread safety는 mutex(간단)나 atomic CAS(lock-free)로 확보합니다.
- RAII wrapper인
PoolUniquePtr로 예외 안전과 자동 해제를 보장합니다. - 통계를 추적하면 capacity tuning이 가능합니다.
- 여러 크기를 다룰 때는 multi-pool로 가며 fragmentation이 일부 발생합니다.
#관련 항목
- Part 3-01: 동적 할당 없이 C++ 쓰기
- Part 3-02: Custom Allocator 기초
- Part 3-04: std::pmr 활용 — 표준 인터페이스 pool
- Part 4-03: Lock-free 기초
- Part 2-02: RAII 실전 패턴 — PoolUniquePtr
#다음 글
Part 3-04: std::pmr 활용 — C++17 표준 polymorphic allocator로, 같은 컨테이너 타입에 다른 pool을 사용합니다.
Embedded C++ for Real Systems · 22 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
범용 HAL 구조 — 벤더 종속성 격리, 다중 보드/MCU 지원, 시리즈 마무리.
Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
UART, SPI, I2C — peripheral을 type-safe class로. Blocking, interrupt, DMA 패턴.
GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
GPIO pin = type — 컴파일 타임에 핀 설정 검증, runtime 비용 0.
이 글을 참조하는 글 (10)
- Lock-free Container 구현 — SPSC Queue·Ring Buffer— Embedded C++ for Real Systems
- 임베디드 Lock-free 기초 — atomic·memory ordering·CAS— Embedded C++ for Real Systems
- Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계— Embedded C++ for Real Systems
- 임베디드 스마트 포인터 선택 — unique·shared·custom 비교— Embedded C++ for Real Systems
- std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석— Embedded C++ for Real Systems
- Custom Allocator 기초 — std::allocator 인터페이스 분석— Embedded C++ for Real Systems
- 동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀— Embedded C++ for Real Systems
- 임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리— Embedded C++ for Real Systems
- 임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장— Embedded C++ for Real Systems
- 임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적— Embedded C++ for Real Systems