C++ 커스텀 Allocator 구현하기
C++ 표준 라이브러리의 모든 컨테이너(vector, list, map 등)는 메모리 관리를 Allocator에 위임합니다. 기본 std::allocator<T>는 new/delete를 사용하지만, 커스텀 Allocator를 작성하면 메모리 풀, 스택 기반 할당, 진단 목적의 추적 등 다양한 전략을 STL 컨테이너에 투명하게 적용할 수 있습니다.
1. 표준 Allocator 인터페이스
섹션 제목: “1. 표준 Allocator 인터페이스”C++11 이후 최소 Allocator 요구사항입니다.
template<typename T>class MyAllocator {public: using value_type = T;
MyAllocator() noexcept = default;
// 리바인딩 생성자 (다른 타입의 allocator로부터 생성) template<typename U> MyAllocator(const MyAllocator<U>&) noexcept {}
T* allocate(std::size_t n); void deallocate(T* ptr, std::size_t n) noexcept;};
template<typename T, typename U>bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept { return true; // stateless일 경우 항상 true}2. 추적 Allocator (Tracking Allocator)
섹션 제목: “2. 추적 Allocator (Tracking Allocator)”메모리 할당/해제를 로깅하는 진단용 Allocator입니다.
#include <memory>#include <iostream>#include <vector>
template<typename T>class TrackingAllocator {public: using value_type = T;
static std::size_t total_allocated; static std::size_t total_deallocated;
TrackingAllocator() noexcept = default;
template<typename U> TrackingAllocator(const TrackingAllocator<U>&) noexcept {}
T* allocate(std::size_t n) { std::size_t bytes = n * sizeof(T); T* ptr = static_cast<T*>(::operator new(bytes)); total_allocated += bytes; std::cout << "[ALLOC] " << bytes << " bytes at " << ptr << "\n"; return ptr; }
void deallocate(T* ptr, std::size_t n) noexcept { std::size_t bytes = n * sizeof(T); total_deallocated += bytes; std::cout << "[FREE] " << bytes << " bytes at " << ptr << "\n"; ::operator delete(ptr); }};
template<typename T>std::size_t TrackingAllocator<T>::total_allocated = 0;template<typename T>std::size_t TrackingAllocator<T>::total_deallocated = 0;
template<typename T, typename U>bool operator==(const TrackingAllocator<T>&, const TrackingAllocator<U>&) noexcept { return true; }
// 사용 예int main() { std::vector<int, TrackingAllocator<int>> v; v.reserve(10); v.push_back(1); v.push_back(2);
std::cout << "Total allocated: " << TrackingAllocator<int>::total_allocated << " bytes\n"; return 0;}3. Pool Allocator
섹션 제목: “3. Pool Allocator”고정 크기 블록을 미리 할당해두고 O(1)로 제공하는 Allocator입니다.
#include <array>#include <cassert>#include <cstddef>
template<typename T, std::size_t N>class PoolAllocator {public: using value_type = T;
PoolAllocator() noexcept { // 프리 리스트 초기화 for (std::size_t i = 0; i < N - 1; ++i) { reinterpret_cast<std::size_t*>(&pool_[i])[0] = i + 1; } reinterpret_cast<std::size_t*>(&pool_[N - 1])[0] = N; // sentinel free_head_ = 0; used_ = 0; }
template<typename U> PoolAllocator(const PoolAllocator<U, N>&) noexcept {}
T* allocate(std::size_t n) { assert(n == 1 && "PoolAllocator supports single-element allocation only"); assert(free_head_ < N && "Pool exhausted");
T* ptr = &pool_[free_head_]; free_head_ = reinterpret_cast<std::size_t*>(ptr)[0]; ++used_; return ptr; }
void deallocate(T* ptr, std::size_t) noexcept { std::size_t idx = ptr - pool_.data(); reinterpret_cast<std::size_t*>(ptr)[0] = free_head_; free_head_ = idx; --used_; }
std::size_t used() const { return used_; }
private: alignas(T) std::array<T, N> pool_; std::size_t free_head_; std::size_t used_;};
template<typename T, std::size_t N, typename U>bool operator==(const PoolAllocator<T,N>&, const PoolAllocator<U,N>&) noexcept { return false; }4. Stack (Arena) Allocator
섹션 제목: “4. Stack (Arena) Allocator”힙 할당 없이 스택 메모리 또는 미리 할당한 버퍼에서 순차적으로 메모리를 제공합니다.
#include <cstddef>#include <cassert>
class ArenaAllocatorBase {public: explicit ArenaAllocatorBase(void* buf, std::size_t size) : buf_(static_cast<char*>(buf)), end_(buf_ + size), cur_(buf_) {}
void* allocate(std::size_t size, std::size_t align) { // 정렬 char* aligned = reinterpret_cast<char*>( (reinterpret_cast<std::uintptr_t>(cur_) + align - 1) & ~(align - 1) ); assert(aligned + size <= end_ && "Arena exhausted"); cur_ = aligned + size; return aligned; }
void reset() { cur_ = buf_; } // 전체 초기화 O(1) std::size_t used() const { return cur_ - buf_; }
private: char* buf_; char* end_; char* cur_;};
template<typename T>class ArenaAllocator {public: using value_type = T;
explicit ArenaAllocator(ArenaAllocatorBase& arena) noexcept : arena_(&arena) {}
template<typename U> ArenaAllocator(const ArenaAllocator<U>& other) noexcept : arena_(other.arena_) {}
T* allocate(std::size_t n) { return static_cast<T*>(arena_->allocate(n * sizeof(T), alignof(T))); }
void deallocate(T*, std::size_t) noexcept { // Arena는 개별 해제를 지원하지 않음 (reset으로 전체 해제) }
ArenaAllocatorBase* arena_;};
template<typename T, typename U>bool operator==(const ArenaAllocator<T>& a, const ArenaAllocator<U>& b) noexcept { return a.arena_ == b.arena_;}
// 사용 예int main() { alignas(64) char buf[4096]; ArenaAllocatorBase arena(buf, sizeof(buf));
std::vector<int, ArenaAllocator<int>> v{ArenaAllocator<int>(arena)}; v.push_back(1); v.push_back(2); v.push_back(3);
std::cout << "Arena used: " << arena.used() << " bytes\n"; arena.reset(); // 전체 해제 O(1) return 0;}5. pmr (Polymorphic Memory Resource) — C++17
섹션 제목: “5. pmr (Polymorphic Memory Resource) — C++17”C++17의 std::pmr 네임스페이스는 런타임 다형성을 활용한 메모리 리소스 시스템입니다.
#include <memory_resource>#include <vector>
int main() { // 스택 버퍼에서 할당 char buf[1024]; std::pmr::monotonic_buffer_resource pool{buf, sizeof(buf)};
std::pmr::vector<int> v{&pool}; for (int i = 0; i < 10; ++i) v.push_back(i);
// pool_options로 성능 튜닝 std::pmr::pool_options opts; opts.max_blocks_per_chunk = 64; std::pmr::synchronized_pool_resource sync_pool{opts};
std::pmr::vector<std::pmr::string> strings{&sync_pool}; strings.emplace_back("hello"); strings.emplace_back("world");
return 0;}6. 성능 비교 가이드라인
섹션 제목: “6. 성능 비교 가이드라인”| Allocator 타입 | 할당 시간 | 해제 시간 | 용도 |
|---|---|---|---|
std::allocator | O(log n) | O(log n) | 일반 목적 |
| Pool Allocator | O(1) | O(1) | 동일 크기 객체 대량 생성 |
| Arena Allocator | O(1) | O(1) (전체만) | 단기 수명 데이터 |
| pmr::monotonic | O(1) | 없음 | 프레임 단위 임시 데이터 |
커스텀 Allocator는 특정 사용 패턴에서 기본 힙 할당기 대비 수십 배의 성능 향상을 가져올 수 있습니다. 특히 게임, 실시간 시스템, 고빈도 거래 시스템처럼 지연 시간이 중요한 환경에서 필수적입니다. C++17의 std::pmr을 우선 검토하고, 요구사항이 더 특수하다면 커스텀 Allocator를 작성하세요.