본문으로 건너뛰기
Embedded C++ for Real Systems · 20/41

Custom Allocator 기초 — std::allocator 인터페이스 분석

· Hawk · 5분 읽기

#한 줄 요약

“Allocator는 메모리 출처를 STL에 알려줍니다.” std::vector<int, MyAllocator> 형태로 내가 지정한 메모리를 사용합니다.

#어떤 문제를 푸는가

std::vector<int>의 기본 allocator는 std::allocator<int>이며 heap(new/malloc)을 사용합니다. 임베디드에서는 heap 회피가 원칙이지만 표준 컨테이너의 편리함을 포기하기는 어렵습니다.

std::vector<int> v;
v.push_back(1); // new 호출 — heap

Custom allocator는 STL과 호환되는 메모리 인터페이스입니다. pool, stack arena, 특정 메모리 영역에서 할당하도록 만들 수 있습니다.

std::vector<int, MyPoolAllocator<int>> v;
v.push_back(1); // pool에서 할당

이 글은 STL allocator concept과 기본 구현을 다룹니다.

#Allocator Concept (C++17 이전)

STL 컨테이너는 allocator concept을 따르는 객체를 사용합니다. C++17 이전의 형태는 이렇습니다.

template<typename T>
class MyAllocator {
public:
using value_type = T;
MyAllocator() noexcept = default;
template<typename U>
MyAllocator(const MyAllocator<U>&) noexcept {}
T* allocate(std::size_t n) {
// 메모리 할당 (n * sizeof(T) 바이트)
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t /*n*/) noexcept {
::operator delete(p);
}
};
template<typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) { return true; }
template<typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) { return false; }

핵심 요구사항은 이렇습니다.

  • value_type typedef를 제공합니다.
  • allocate(n)이 n * sizeof(T) 바이트를 반환합니다.
  • deallocate(p, n)으로 메모리를 돌려줍니다.
  • operator==, operator!=로 컨테이너끼리 비교가 가능합니다.

C++20에서는 concept으로 강제됩니다(std::allocator_traits).

#가장 단순한 예 — Pool Allocator

고정 크기 pool에서 할당하는 기본형입니다.

template<typename T, size_t N>
class PoolAllocator {
public:
using value_type = T;
PoolAllocator() noexcept = default;
template<typename U>
PoolAllocator(const PoolAllocator<U, N>&) noexcept {}
T* allocate(std::size_t n) {
if (n != 1) throw std::bad_alloc(); // 한 번에 1개만
for (size_t i = 0; i < N; ++i) {
if (!in_use_[i]) {
in_use_.set(i);
return reinterpret_cast<T*>(&storage_[sizeof(T) * i]);
}
}
throw std::bad_alloc();
}
void deallocate(T* p, std::size_t /*n*/) noexcept {
size_t i = (reinterpret_cast<std::byte*>(p) - storage_) / sizeof(T);
if (i < N) in_use_.reset(i);
}
private:
static inline alignas(T) std::byte storage_[sizeof(T) * N]{};
static inline std::bitset<N> in_use_{};
};

문제는 throw입니다. -fno-exceptions 환경에서는 다른 패턴이 필요합니다.

#No-exception allocator — abort 사용

-fno-exceptions에서는 throw 대신 abort 또는 null을 사용합니다.

template<typename T, size_t N>
class NoExceptPoolAllocator {
public:
using value_type = T;
T* allocate(std::size_t n) noexcept {
if (n != 1) std::abort();
for (size_t i = 0; i < N; ++i) {
if (!in_use_[i]) {
in_use_.set(i);
return reinterpret_cast<T*>(&storage_[sizeof(T) * i]);
}
}
std::abort(); // 또는 다른 fail 메커니즘
}
void deallocate(T* p, std::size_t) noexcept {
size_t i = (reinterpret_cast<std::byte*>(p) - storage_) / sizeof(T);
if (i < N) in_use_.reset(i);
}
// ...
};

표준 STL은 allocate가 throw하거나 정상 반환하는 것을 가정합니다. abort나 null을 쓰면 STL 호환이 깨지므로 ETL이나 직접 구현한 컨테이너를 권장합니다.

#C++17 — std::pmr::polymorphic_allocator

C++17이 polymorphic allocator를 도입하면서 런타임에 allocator를 바꿀 수 있게 됐습니다.

#include <memory_resource>
std::pmr::vector<int> v(&my_pool);
// 같은 std::pmr::vector<int> 타입이지만
// 런타임에 다른 pool 사용 가능

자세한 내용은 Part 3-04: std::pmr 활용.

#Stateful allocator

allocator가 pool 포인터나 arena 같은 상태를 가지는 경우입니다.

template<typename T>
class ArenaAllocator {
Arena* arena_;
public:
using value_type = T;
explicit ArenaAllocator(Arena* arena) : arena_(arena) {}
template<typename U>
ArenaAllocator(const ArenaAllocator<U>& other) : arena_(other.arena()) {}
Arena* arena() const { return arena_; }
T* allocate(std::size_t n) {
return static_cast<T*>(arena_->allocate(n * sizeof(T), alignof(T)));
}
void deallocate(T* p, std::size_t /*n*/) noexcept {
// arena는 한 번에 해제 — 개별 deallocate 무의미
}
};
template<typename T, typename U>
bool operator==(const ArenaAllocator<T>& a, const ArenaAllocator<U>& b) {
return a.arena() == b.arena();
}

상태 있는 allocator는 컨테이너 간 비교가 의미를 가집니다. 같은 arena를 사용하는 두 vector만 동등합니다.

#Arena allocator — bump pointer

가장 단순한 stateful allocator입니다. 포인터를 증가시키며 할당하므로 개별 해제는 불가합니다.

class Arena {
std::byte* base_;
std::byte* current_;
std::byte* end_;
public:
Arena(std::byte* buffer, size_t size)
: base_(buffer), current_(buffer), end_(buffer + size) {}
void* allocate(size_t bytes, size_t alignment) {
// align 맞추기
auto* aligned = reinterpret_cast<std::byte*>(
(reinterpret_cast<uintptr_t>(current_) + alignment - 1) & ~(alignment - 1));
if (aligned + bytes > end_) return nullptr; // 부족
current_ = aligned + bytes;
return aligned;
}
void reset() { current_ = base_; } // 한 번에 모두 해제
size_t used() const { return current_ - base_; }
};
std::array<std::byte, 4096> buffer;
Arena arena(buffer.data(), buffer.size());

포인터 산술만으로 끝나므로 할당이 매우 빠르고 fragmentation은 0입니다. 단점은 개별 해제가 불가하다는 점입니다. frame allocator나 transient 객체에 적합합니다.

#임베디드 — Static memory pool

전역 또는 .bss의 고정 메모리 영역을 pool로 활용합니다.

// 한 task의 transient allocation 용
template<typename T>
class TaskArenaAllocator {
static inline std::array<std::byte, 4096> arena_storage_{};
static inline Arena arena_{arena_storage_.data(), arena_storage_.size()};
public:
using value_type = T;
TaskArenaAllocator() = default;
template<typename U> TaskArenaAllocator(const TaskArenaAllocator<U>&) {}
T* allocate(std::size_t n) noexcept {
auto* p = arena_.allocate(n * sizeof(T), alignof(T));
if (!p) std::abort();
return static_cast<T*>(p);
}
void deallocate(T*, std::size_t) noexcept {
// no-op (arena가 reset으로 일괄 해제)
}
static void reset() { arena_.reset(); }
};
// 사용
void process_frame() {
std::vector<int, TaskArenaAllocator<int>> v;
v.push_back(1); // arena에서
v.push_back(2);
// 함수 끝
TaskArenaAllocator<int>::reset(); // 한 번에 해제
}

프레임 단위로 할당과 해제를 처리합니다. 매 프레임 시작 시점에 reset합니다.

#Allocator로 STL 컨테이너 사용

// 정수 vector — pool에서 할당
std::vector<int, PoolAllocator<int, 32>> v;
v.push_back(1);
v.push_back(2);
// v가 사라지면 pool 자리 회수
// map — pool에서
std::map<int, Order, std::less<>, PoolAllocator<std::pair<const int, Order>, 16>> m;
m[1] = Order{};
// string — pool에서
std::basic_string<char, std::char_traits<char>, PoolAllocator<char, 256>> s;
s = "hello";

STL 인터페이스를 그대로 쓰면서 메모리를 제어할 수 있습니다. 단점은 컨테이너 타입이 길어진다는 점입니다.

C++17의 std::pmr가 훨씬 깔끔합니다. 같은 타입(std::pmr::vector<int>)으로 두고 런타임에 분기합니다.

#C++20 allocator concept

std::allocator_traits가 모든 기본 정의를 제공합니다. 최소 구현은 이렇습니다.

template<typename T>
struct MinimalAllocator {
using value_type = T;
T* allocate(std::size_t n) { /* */ }
void deallocate(T* p, std::size_t n) { /* */ }
// 나머지는 allocator_traits가 default
};
// 사용
std::vector<int, MinimalAllocator<int>> v;

C++17 이전에는 수많은 typedef가 필요했지만 이제 value_type, allocate, deallocate 셋이면 충분합니다.

#자주 보는 함정과 안티패턴

#1. rebind 누락

std::list<int, MyAllocator<int>> l;
// list 내부에 ListNode<int> 할당 필요
// rebind로 MyAllocator<ListNode<int>> 만들어야

allocator_traits가 자동으로 rebind해 주므로 C++17 이후로는 보통 신경 쓰지 않아도 됩니다.

#2. allocator 비교 잘못

bool operator==(const MyAlloc&, const MyAlloc&) { return true; } // 다른 pool도 같다고?

상태 있는 allocator는 상태 자체를 비교해야 합니다. 잘못된 비교는 컨테이너 move/swap을 망칩니다.

#3. deallocate에서 size 무시

void deallocate(T* p, size_t n) {
free(p); // n 무시 — array는 어떻게?
}

allocator에 따라 size가 필요합니다. pool은 무시해도 되지만 다른 경우엔 꼭 활용합니다.

#4. throw 사용

-fno-exceptions 환경에서 throw가 발생하면 abort로 떨어집니다. abort를 명시하거나 예외를 활성화합니다.

#5. thread safety 가정

multi-thread에서 같은 pool을 동시에 접근하면 race가 발생합니다. mutex나 thread-local pool을 씁니다.

#6. 복잡한 allocator

간단한 pool로 충분한데 generic allocator를 만드는 경우가 많습니다. 프로젝트 요구에 맞게 단순하게 유지합니다.

#측정 — Pool vs heap allocator

같은 std::vector<int>로 10000회 push_back을 비교합니다.

# heap (std::allocator)
allocation: 변동 (realloc 시점)
total time: 1.2 ms
fragmentation: 시간에 따라 증가
# pool (fixed size)
allocation: 일정 (pool slot 1개)
total time: 0.4 ms
fragmentation: 0

Pool이 약 3배 빠르고 결정적입니다. 임베디드 real-time에서 중요한 차이입니다.

#정리

  • Custom allocator는 STL의 메모리 출처를 제어하며 value_type, allocate, deallocate 세 가지만 구현하면 됩니다.
  • Stateful allocator는 pool/arena 포인터를 보유하므로 컨테이너 간 비교에 의미가 있습니다.
  • Arena allocator는 bump pointer 방식이라 개별 해제는 불가하지만 일괄 reset이 가능해 transient 객체에 적합합니다.
  • -fno-exceptions 환경에서는 throw 대신 abort를 사용하며 STL 표준 호환이 깨질 수 있습니다.
  • C++17 std::pmr이 훨씬 단순하며 다음 chapter에서 다룹니다.

#관련 항목

#다음 글

Part 3-03: Pool Allocator 구현 — 임베디드의 대표적 allocator로, 고정 크기 블록과 free list 관리를 다룹니다.

Embedded C++ for Real Systems · 21 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교