std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
#한 줄 요약
“
std::pmr::vector<int>는 런타임에 allocator를 선택할 수 있습니다.” 전통적인std::vector<int, Alloc>처럼 타입에 박히지 않습니다.
#어떤 문제를 푸는가
Part 3-02 방식의 custom allocator는 allocator가 타입에 박힙니다.
std::vector<int, PoolAllocator<int, 16>> v1; // pool 16std::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() 호출 시 일괄 resetPart 3-02의 Arena와 같은 아이디어이며, frame allocator에 적합합니다.
#임베디드 — Static buffer pool
// 전역 static bufferconstexpr 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); // OKprocess(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::stringstd::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_equal이 dynamic_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 cyclespmr이 코드는 작고 런타임은 약간 느립니다. 대부분 작은 차이이며, 코드 크기가 중요한 임베디드에서는 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-02: Custom Allocator 기초 — 비-pmr allocator
- Part 3-03: Pool Allocator — pool 자체 구현
- Part 3-01: 동적 할당 없이 — 정적 우선
- Part 1-08: C++ 표준 선택 — C++17
#다음 글
Part 3-05: No-Exception 설계 — -fno-exceptions 환경에서 에러를 처리하는 패턴을 다룹니다. std::optional, error code, expected가 등장합니다.
Embedded C++ for Real Systems · 23 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 비교
관련 글
임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
C++11/14/17/20/23 — 임베디드에서 어느 표준을 골라야 하나. 컴파일러 지원, 표준 라이브러리 변화, 핵심 기능 비교.
임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
범용 HAL 구조 — 벤더 종속성 격리, 다중 보드/MCU 지원, 시리즈 마무리.
Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
UART, SPI, I2C — peripheral을 type-safe class로. Blocking, interrupt, DMA 패턴.