콘텐츠로 이동

C++ 커스텀 Allocator 구현하기

C++ 표준 라이브러리의 모든 컨테이너(vector, list, map 등)는 메모리 관리를 Allocator에 위임합니다. 기본 std::allocator<T>new/delete를 사용하지만, 커스텀 Allocator를 작성하면 메모리 풀, 스택 기반 할당, 진단 목적의 추적 등 다양한 전략을 STL 컨테이너에 투명하게 적용할 수 있습니다.


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
}

메모리 할당/해제를 로깅하는 진단용 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;
}

고정 크기 블록을 미리 할당해두고 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; }

힙 할당 없이 스택 메모리 또는 미리 할당한 버퍼에서 순차적으로 메모리를 제공합니다.

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

Allocator 타입할당 시간해제 시간용도
std::allocatorO(log n)O(log n)일반 목적
Pool AllocatorO(1)O(1)동일 크기 객체 대량 생성
Arena AllocatorO(1)O(1) (전체만)단기 수명 데이터
pmr::monotonicO(1)없음프레임 단위 임시 데이터

커스텀 Allocator는 특정 사용 패턴에서 기본 힙 할당기 대비 수십 배의 성능 향상을 가져올 수 있습니다. 특히 게임, 실시간 시스템, 고빈도 거래 시스템처럼 지연 시간이 중요한 환경에서 필수적입니다. C++17의 std::pmr을 우선 검토하고, 요구사항이 더 특수하다면 커스텀 Allocator를 작성하세요.