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

std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석

· Hawk · 4분 읽기

#한 줄 요약

std::pmr::vector<int>는 런타임에 allocator를 선택할 수 있습니다.” 전통적인 std::vector<int, Alloc>처럼 타입에 박히지 않습니다.

#어떤 문제를 푸는가

Part 3-02 방식의 custom allocator는 allocator가 타입에 박힙니다.

std::vector<int, PoolAllocator<int, 16>> v1; // pool 16
std::vector<int, PoolAllocator<int, 32>> v2; // pool 32 — 다른 타입
// v1과 v2는 *다른 타입* — 호환 안 됨

함수 매개변수도 서로 다른 vector 타입이 됩니다. 함수마다 별도로 인스턴스화되어 코드가 부풉니다.

C++17의 std::pmr(Polymorphic Memory Resource)이 이를 해결합니다.

std::pmr::vector<int> v1(&pool1); // pool1에서
std::pmr::vector<int> v2(&pool2); // pool2에서
// v1과 v2는 *같은 타입* — 호환
void process(std::pmr::vector<int>& v) {
// 어떤 pool 위 vector든 받음
}

런타임 polymorphism으로 컨테이너 타입은 동일하게 유지하면서 코드 인스턴스는 한 번만 만들어집니다.

#핵심 — memory_resource

std::pmr::memory_resource는 abstract base class이며 virtual interface를 노출합니다.

class memory_resource {
public:
virtual ~memory_resource() = default;
void* allocate(size_t bytes, size_t alignment) {
return do_allocate(bytes, alignment);
}
void deallocate(void* p, size_t bytes, size_t alignment) {
do_deallocate(p, bytes, alignment);
}
bool is_equal(const memory_resource& other) const {
return do_is_equal(other);
}
protected:
virtual void* do_allocate(size_t, size_t) = 0;
virtual void do_deallocate(void*, size_t, size_t) = 0;
virtual bool do_is_equal(const memory_resource&) const noexcept = 0;
};

virtual 함수가 3개이며 vptr 1개(4 byte)가 추가됩니다.

std::pmr::polymorphic_allocator<T>가 memory_resource를 wrap해 STL allocator interface를 제공합니다.

#표준 memory_resource 구현

C++17은 세 가지 기본 resource를 제공합니다.

#1. new_delete_resource — heap

auto* mr = std::pmr::new_delete_resource();
std::pmr::vector<int> v(mr); // heap에서

기본 std::pmr::vector<int>의 default resource입니다. 임베디드에서는 회피합니다.

#2. null_memory_resource — 항상 실패

auto* mr = std::pmr::null_memory_resource();
std::pmr::vector<int> v(mr);
v.push_back(1); // throws std::bad_alloc

실수 방지용입니다. heap 사용을 원치 않을 때 base resource로 둡니다.

#3. monotonic_buffer_resource — bump pointer

미리 준비된 buffer에 순차적으로 할당하며, 개별 해제는 불가합니다.

std::array<std::byte, 4096> buffer;
std::pmr::monotonic_buffer_resource mr(buffer.data(), buffer.size());
std::pmr::vector<int> v(&mr);
v.reserve(100);
for (int i = 0; i < 100; ++i) v.push_back(i);
// 모든 메모리는 stack buffer에서
// mr 소멸 또는 release() 호출 시 일괄 reset

Part 3-02의 Arena와 같은 아이디어이며, frame allocator에 적합합니다.

#임베디드 — Static buffer pool

// 전역 static buffer
constexpr size_t kPoolSize = 8192;
alignas(std::max_align_t) std::byte g_pool_buffer[kPoolSize];
std::pmr::monotonic_buffer_resource g_pool(g_pool_buffer, kPoolSize);
void process_request() {
std::pmr::vector<Order> orders(&g_pool);
orders.reserve(10);
for (auto& req : input_requests) {
orders.push_back(Order{req});
}
process_orders(orders);
// 함수 끝 — orders는 소멸하지만 buffer는 안 비워짐
// 다음 호출에 release 또는 reset 필요
}
void reset_pool() {
g_pool.release(); // 또는 buffer 재할당
}

매 frame 또는 batch 시작 시점에 release합니다. fragmentation이 없고 빠릅니다.

#폴백 chain — unsynchronized_pool_resource

C++17은 복잡한 pool도 제공하지만, 임베디드 기준으로는 너무 무거운 경우가 많습니다.

std::pmr::pool_options opts{
.max_blocks_per_chunk = 16,
.largest_required_pool_block = 256
};
std::pmr::unsynchronized_pool_resource pool(opts);
std::pmr::vector<int> v(&pool);

보통은 직접 구현한 pool이 더 작고 빠릅니다. 표준 제공 pool은 데스크톱 기본 환경을 지향합니다.

#Custom memory_resource — 직접 정의

class PoolMemoryResource : public std::pmr::memory_resource {
Pool* pool_;
public:
explicit PoolMemoryResource(Pool* pool) : pool_(pool) {}
protected:
void* do_allocate(size_t bytes, size_t alignment) override {
return pool_->allocate(bytes, alignment);
}
void do_deallocate(void* p, size_t /*bytes*/, size_t /*align*/) override {
pool_->deallocate(p);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
auto* o = dynamic_cast<const PoolMemoryResource*>(&other);
return o && o->pool_ == pool_;
}
};
// 사용
Pool my_pool;
PoolMemoryResource mr(&my_pool);
std::pmr::vector<int> v(&mr);

dynamic_cast는 RTTI를 필요로 합니다. -fno-rtti 환경에서는 다른 방식으로 비교해야 합니다(typeid 회피).

#RTTI 없는 비교

class PoolMemoryResource : public std::pmr::memory_resource {
Pool* pool_;
static inline int s_type_id; // unique tag
public:
static const void* type_id() { return &s_type_id; }
explicit PoolMemoryResource(Pool* pool) : pool_(pool) {}
virtual const void* get_type_id() const { return &s_type_id; }
protected:
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
auto* o = static_cast<const PoolMemoryResource*>(&other);
if (o->get_type_id() != &s_type_id) return false;
return o->pool_ == pool_;
}
};

get_type_id() virtual로 type을 식별하면 RTTI 없이도 type-safe하게 비교할 수 있습니다.

#함수 매개변수에 pmr

std::pmr::vector는 type이 통일되므로 함수 매개변수에 자연스럽게 들어갑니다.

// 어떤 pmr vector든 받음
void process(std::pmr::vector<int>& v) {
v.push_back(1);
}
std::pmr::vector<int> v1(&pool1);
std::pmr::vector<int> v2(&pool2);
process(v1); // OK
process(v2); // OK — 같은 함수, 다른 메모리

반면 non-pmr 버전은 이렇게 됩니다.

template<typename Alloc>
void process(std::vector<int, Alloc>& v) { // 매 호출마다 인스턴스화
v.push_back(1);
}

std::vector는 allocator마다 별도의 instance가 만들어지므로 코드가 부풉니다.

#임베디드 패턴 — Frame allocator

매 frame 한 번씩 reset해서 transient 객체에 활용하는 패턴입니다.

constexpr size_t kFrameBufferSize = 16 * 1024;
alignas(std::max_align_t) static std::byte frame_buffer[kFrameBufferSize];
static std::pmr::monotonic_buffer_resource frame_pool(
frame_buffer, kFrameBufferSize);
void process_frame() {
// 매 frame 시작 시 reset
frame_pool.release();
std::pmr::vector<Object> objects(&frame_pool);
std::pmr::string log_buffer(&frame_pool);
// 처리
for (auto& obj : input) {
objects.push_back(process_one(obj));
log_buffer += format_log(obj);
}
// 함수 끝 — 객체들이 소멸하지만 메모리는 reset에서 회수
}

매우 빠른 alloc과 dealloc이 가능하고 deterministic합니다.

#std::pmr 가용 컨테이너

C++17 <memory_resource>가 제공하는 std::pmr::* 컨테이너는 다음과 같습니다.

std::pmr::vector<T>
std::pmr::string
std::pmr::list<T>
std::pmr::map<K, V>
std::pmr::set<T>
std::pmr::unordered_map<K, V>
std::pmr::unordered_set<T>
std::pmr::deque<T>

대부분의 표준 STL 컨테이너에 pmr 버전이 존재합니다.

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

#1. Default resource 사용

std::pmr::vector<int> v; // default resource = heap

명시적으로 resource를 지정해 heap을 회피합니다.

#2. monotonic resource 미release

void process() {
std::pmr::vector<int> v(&pool);
v.push_back(1);
// 함수 끝 — v 소멸, 그러나 pool 메모리는 안 비워짐
}
// pool이 계속 차오름

명시적으로 reset이나 release를 호출합니다.

#3. Resource lifetime 부적절

std::pmr::vector<int>* make() {
std::pmr::monotonic_buffer_resource pool(...); // local
return new std::pmr::vector<int>(&pool); // pool 소멸 → dangling
}

resource는 vector보다 오래 살아 있어야 합니다.

#4. Virtual 호출 비용

std::pmr은 virtual function을 호출하므로 간접 호출 비용이 듭니다. 극히 hot path에서만 측정해 판단하고 대부분 무시할 만합니다.

#5. RTTI 의존 비교

default do_is_equaldynamic_cast를 사용합니다. RTTI를 끄면 우회가 필요합니다.

#6. bytes/alignment 인자 무시

void do_deallocate(void* p, size_t, size_t) {
free(p); // bytes 무시 — pool은 어떻게 알지?
}

pool 구현에 따라 bytes가 필요하거나 자체 추적이 필요합니다.

#측정 — pmr vs 직접 allocator

같은 vector를 std::vector<int, MyAlloc>std::pmr::vector<int>로 비교합니다.

# std::vector<int, MyAlloc> (3 allocator 인스턴스)
.text : +2.4 KB (3개 vector specialization)
runtime alloc: 직접 호출, ~10 cycles
# std::pmr::vector<int> (3 resource)
.text : +0.8 KB (한 vector specialization)
runtime alloc: virtual 호출, ~15 cycles

pmr이 코드는 작고 런타임은 약간 느립니다. 대부분 작은 차이이며, 코드 크기가 중요한 임베디드에서는 pmr이 유리합니다.

#정리

  • std::pmr(C++17)은 polymorphic memory resource로, 런타임에 allocator를 선택합니다.
  • std::pmr::vector<int>는 type을 통일하면서 다른 resource를 사용할 수 있습니다.
  • 표준 resource는 세 가지입니다 — new_delete(heap), null(실패), monotonic(bump pointer).
  • Custom resource는 memory_resource를 상속하고 virtual 함수 3개를 구현합니다.
  • 임베디드 패턴은 static buffer와 monotonic_buffer_resource를 frame 단위로 reset하는 것입니다.
  • RTTI 없는 환경에서는 do_is_equal에 직접 type id를 다룹니다.

#관련 항목

#다음 글

Part 3-05: No-Exception 설계-fno-exceptions 환경에서 에러를 처리하는 패턴을 다룹니다. std::optional, error code, expected가 등장합니다.

Embedded C++ for Real Systems · 23 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 비교