C++ std::pmr 완벽 가이드 | Polymorphic Memory Resources로 메모리 풀
이 글의 핵심
C++ std::pmr 완벽 가이드에 대한 실전 가이드입니다. Polymorphic Memory Resources로 메모리 풀 등을 예제와 함께 상세히 설명합니다.
들어가며: malloc/new가 병목일 때
”할당/해제가 너무 자주 일어난다”
15번·32번에서 성능과 메모리를 다뤘다면, 할당 자체가 프로파일에서 상위를 차지할 때가 있습니다. 작은 객체를 많이 할당/해제하면 힙 단편화(작은 빈 공간이 흩어져 큰 연속 메모리 할당이 어려워지는 현상)와 할당자 오버헤드가 누적됩니다.
메모리 풀(미리 할당한 큰 블록에서 조각을 나눠 주어 할당 횟수를 줄이는 방식)은 미리 큰 블록을 한 번 할당하고, 그 안에서 작은 조각을 나눠 주어 할당 횟수를 줄입니다. C++17 std::pmr(polymorphic memory resources)는 std::memory_resource를 기반으로 컨테이너에 주입 가능한 할당자를 제공해, 같은 타입의 컨테이너를 서로 다른 풀에 연결할 수 있게 합니다.
이 글에서 다루는 것:
- 문제 시나리오: 프로파일에서 malloc이 상위를 차지할 때
- std::pmr 완전 예제: monotonic_buffer_resource, pool_resource, 커스텀 memory_resource, pmr 컨테이너
- 흔한 에러와 해결법: 수명 관리, 리소스 혼용, alignment
- 베스트 프랙티스: 풀 선택 가이드, 점진적 도입
- 프로덕션 패턴: 프레임 풀, 요청 스코프, 게임 엔티티
개념을 잡는 비유
메모리 풀·PMR은 창고 칸을 미리 나눠 두고 필요할 때만 꺼내 쓰는 방식에 가깝습니다. 할당·해제 패턴이 고정돼 있으면 전역 new보다 예측 가능하고 캐시 친화적일 수 있습니다.
목차
- 문제 시나리오: malloc이 병목일 때
- 메모리 풀 개념
- std::pmr 개요
- monotonic_buffer_resource 완전 예제
- pool_resource 완전 예제
- 커스텀 memory_resource 구현
- pmr 컨테이너 실전 활용
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 성능 벤치마크
- 프로덕션 패턴
- 정리
1. 문제 시나리오: malloc이 병목일 때
실제 겪는 상황
"프로파일러에서 malloc/free가 전체 실행 시간의 30%를 차지해요."
"작은 객체를 수만 개 할당하다 보니 힙 단편화로 OOM이 나요."
"게임 프레임마다 엔티티를 생성/삭제하는데 프레임 드랍이 심해요."
"HTTP 요청 처리 시마다 파싱 결과를 할당하는데, 동시 요청이 많으면 지연이 커요."
"파서가 std::vector, std::map을 반복 생성하는데 메모리 할당이 폭발해요."
원인 후보
- 할당 횟수 과다: 작은 객체를 반복 할당/해제 → malloc 오버헤드 누적
- 힙 단편화: 작은 빈 공간이 흩어져 큰 연속 메모리 할당 실패
- 캐시 미스: 할당된 메모리가 물리적으로 흩어져 캐시 효율 저하
- 락 경합: 멀티스레드에서 전역 힙 락 경합
메모리 풀은 1~3번을 완화하고, 스레드별 풀은 4번을 완화합니다.
시나리오별 해결 방향
| 시나리오 | 특징 | 권장 리소스 |
|---|---|---|
| 게임 프레임 | 매 프레임 생성/해제, 수명이 짧음 | monotonic_buffer_resource + release() |
| HTTP 요청 | 요청 단위로 할당, 요청 끝에 해제 | monotonic_buffer_resource |
| 노드 기반 자료구조 | 고정 크기 노드 반복 할당/해제 | synchronized_pool_resource |
| 장기 객체 | 수명이 길고 크기가 다양함 | 기본 힙 또는 pool_resource |
| 파싱/직렬화 | 임시 버퍼 다수, 스코프 끝에 해제 | monotonic_buffer_resource |
Before/After: HTTP 파서 예시
Before (기본 할당): 매 요청마다 vector, map, string이 힙에서 반복 할당됩니다.
// ❌ 기본 할당 — malloc 호출 다수
void handleRequest(const std::string& raw) {
std::vector<std::string> path_segments;
std::map<std::string, std::string> headers;
std::string body;
parsePath(raw, path_segments);
parseHeaders(raw, headers);
parseBody(raw, body);
process(path_segments, headers, body);
}
After (pmr 적용): 요청 스코프 풀에서 한 번에 할당하고, 함수 종료 시 풀과 함께 해제됩니다.
// ✅ pmr — 할당 횟수·단편화 감소
void handleRequest(const std::string& raw) {
std::pmr::monotonic_buffer_resource request_pool;
std::pmr::vector<std::pmr::string> path_segments(&request_pool);
std::pmr::map<std::pmr::string, std::pmr::string, std::less<>>
headers(&request_pool);
std::pmr::string body(&request_pool);
parsePath(raw, path_segments);
parseHeaders(raw, headers);
parseBody(raw, body);
process(path_segments, headers, body);
} // request_pool 소멸 → 모든 할당 일괄 해제
2. 메모리 풀 개념
할당 횟수를 줄이기
- 풀은 큰 메모리 블록을 한 번(또는 소수 번) 할당한 뒤, 그 안에서 고정 크기 또는 가변 크기 슬롯을 나눠 줍니다. 해제 시 풀에 반환만 하고, 실제 free는 풀 소멸 시 한 번만 합니다.
- 고정 크기 풀: 같은 크기의 객체만 할당할 때(노드 풀, 이벤트 풀 등) 단순하고 단편화가 적습니다.
- 가변 크기: 여러 크기를 지원하려면 슬롯 관리가 복잡해지므로, monotonic_buffer_resource처럼 “앞에서만 잘라 쓰고 해제는 안 하는” 방식이 구현이 쉽고, 프레임/요청 단위로 리셋하는 용도에 맞습니다.
메모리 풀 동작 흐름
flowchart TB
subgraph Heap["전역 힙"]
H[한 번 큰 블록 할당]
end
subgraph Pool["메모리 풀"]
B[블록]
B --> S1[슬롯 1]
B --> S2[슬롯 2]
B --> S3[슬롯 3]
end
H --> B
S1 --> A1[객체 A]
S2 --> A2[객체 B]
S3 --> A3[객체 C]
monotonic vs pool 비교
flowchart LR
subgraph Mono["monotonic_buffer_resource"]
M1[할당1] --> M2[할당2] --> M3[할당3]
M3 -.->|release 시 전체 반환| M1
end
subgraph Pool["pool_resource"]
P1[슬롯] <--> P2[슬롯]
P2 <--> P3[슬롯]
end
HTTP 요청 처리 시퀀스 (pmr 적용 시)
sequenceDiagram
participant Req as handleRequest
participant Pool as monotonic_buffer_resource
participant Vec as pmr::vector
participant Map as pmr::map
Req->>Pool: 생성 (스코프 진입)
Req->>Vec: path_segments(&pool)
Req->>Map: headers(&pool)
Req->>Vec: push_back, parsePath...
Req->>Map: insert, parseHeaders...
Req->>Req: process()
Req->>Pool: 소멸 (스코프 종료)
Note over Pool: 모든 할당 일괄 해제
3. std::pmr 개요
왜 std::pmr인가?
기존 std::allocator는 템플릿 타입으로 컴파일 타임에 고정됩니다. std::vector<int, MyAllocator<int>>와 std::vector<int, OtherAllocator<int>>는 서로 다른 타입이어서 같은 함수에 넘기기 어렵습니다. 반면 std::pmr::polymorphic_allocator는 runtime에 memory_resource*를 가리키므로, 같은 std::pmr::vector<int> 타입이라도 다른 풀을 사용할 수 있습니다. 이렇게 하면 API는 std::pmr::vector 하나로 통일해 두고, 호출하는 쪽에서 풀만 바꿔 주면 됩니다.
Polymorphic Allocator
- std::pmr::memory_resource는 순수 가상 인터페이스: allocate, deallocate, is_equal만 정의합니다. 구현체(pool_resource, monotonic_buffer_resource 등)를 런타임에 선택할 수 있습니다.
- std::pmr::polymorphic_allocator<T>는 이 memory_resource를 가리키는 할당자로, std::vector<T, std::pmr::polymorphic_allocator<T>>처럼 컨테이너에 넘기면 해당 리소스에서 할당합니다.
- std::pmr::vector는 std::vector<T, std::pmr::polymorphic_allocator<T>>의 별칭입니다. 생성자에서 memory_resource*를 받아 그 풀을 사용합니다.
std::pmr 아키텍처 다이어그램
flowchart TB
subgraph Container["pmr 컨테이너"]
V["std pmr vector"]
M["std pmr map"]
S["std pmr string"]
end
subgraph Alloc["polymorphic_allocator"]
PA[memory_resource*]
end
subgraph Resources["memory_resource 구현체"]
MONO[monotonic_buffer_resource]
POOL[synchronized_pool_resource]
CUSTOM[커스텀 리소스]
end
subgraph Backend["백엔드"]
HEAP[전역 힙]
BUF[스택/정적 버퍼]
end
V --> PA
M --> PA
S --> PA
PA --> MONO
PA --> POOL
PA --> CUSTOM
MONO --> BUF
POOL --> HEAP
CUSTOM --> HEAP
빌드 및 실행
# C++17 이상 필요
g++ -std=c++17 -O2 -o pmr_demo pmr_demo.cpp
./pmr_demo
기본 사용 예
#include <memory_resource>
#include <vector>
int main() {
char buffer[1024];
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::vector<int> v(&pool);
v.push_back(1);
v.push_back(2);
// v의 할당은 buffer 안에서 이루어짐
}
4. monotonic_buffer_resource 완전 예제
개념: 앞에서만 잘라 쓰기
- std::pmr::monotonic_buffer_resource: 주어진 버퍼(또는 업스트림 리소스)에서 순차적으로 메모리를 잘라 씁니다. deallocate는 no-op이라, 개별 해제는 하지 않고 리셋으로 전체를 되돌립니다. 프레임 버퍼, 요청 스코프용으로 적합합니다.
예제 1: 스택 버퍼 + monotonic (힙 할당 제로)
#include <memory_resource>
#include <vector>
#include <array>
void processRequest() {
// 스택에 64KB 버퍼 할당 — 힙 사용 없음
std::array<std::byte, 65536> stack_buffer;
std::pmr::monotonic_buffer_resource pool{
stack_buffer.data(), stack_buffer.size(),
std::pmr::new_delete_resource() // 오버플로 시 힙 사용
};
std::pmr::vector<int> ids(&pool);
std::pmr::vector<std::pmr::string> tokens(&pool);
for (int i = 0; i < 1000; ++i) {
ids.push_back(i);
tokens.push_back("token");
}
// 함수 종료 시 stack_buffer와 pool 자동 해제
}
예제 2: 프레임 풀 + release()
#include <memory_resource>
#include <vector>
struct Entity { int id; float x, y; };
struct Component { int type; void* data; };
void gameLoop() {
std::array<std::byte, 1024*1024> frame_buffer;
std::pmr::monotonic_buffer_resource frame_pool{
frame_buffer.data(), frame_buffer.size(),
std::pmr::new_delete_resource()
};
while (running) {
frame_pool.release(); // 이전 프레임 메모리 리셋
std::pmr::vector<Entity> entities(&frame_pool);
std::pmr::vector<Component> components(&frame_pool);
// ... 엔티티 생성, 프레임 로직 ...
}
}
예제 3: 여러 pmr 컨테이너가 같은 풀 공유
#include <memory_resource>
#include <vector>
#include <string>
#include <map>
int main() {
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> nums(&pool);
std::pmr::vector<std::pmr::string> names(&pool);
std::pmr::map<std::pmr::string, int, std::less<>> scores(&pool);
nums.push_back(42);
names.push_back("Alice");
scores["Bob"] = 100;
// 모두 pool에서 할당
}
5. pool_resource 완전 예제
개념: 고정 크기 블록 재사용
- std::pmr::synchronized_pool_resource / unsynchronized_pool_resource: 고정 크기 블록 풀을 관리하며, 해제한 블록을 재사용합니다. pool_options로 블록 크기·최대 블록 수 등을 조정할 수 있습니다.
예제 1: pool_options로 블록 크기 조정
#include <memory_resource>
int main() {
std::pmr::pool_options opts;
opts.max_blocks_per_chunk = 32; // 청크당 최대 블록 수
opts.largest_required_pool_block = 256; // 최대 블록 크기
std::pmr::synchronized_pool_resource pool{opts};
// 256바이트 이하 객체에 적합, 멀티스레드 안전
}
예제 2: 노드 기반 자료구조에 pool 적용
#include <memory_resource>
#include <list>
struct TreeNode {
int value;
TreeNode* left = nullptr;
TreeNode* right = nullptr;
};
void buildTree(std::pmr::memory_resource* mr) {
std::pmr::list<TreeNode> nodes(mr);
for (int i = 0; i < 1000; ++i) {
nodes.push_back(TreeNode{i});
}
// 노드들이 pool에서 할당, list 소멸 시 pool에 반환
}
int main() {
std::pmr::synchronized_pool_resource pool;
buildTree(&pool);
}
예제 3: unsynchronized_pool (단일 스레드 전용)
#include <memory_resource>
void singleThreadWork() {
// 단일 스레드에서만 사용 — 락 오버헤드 없음
std::pmr::unsynchronized_pool_resource pool;
std::pmr::vector<int> v(&pool);
for (int i = 0; i < 10000; ++i) {
v.push_back(i);
}
}
6. 커스텀 memory_resource 구현
예제 1: 로깅 memory_resource
할당/해제를 로깅하는 디버깅용 리소스입니다.
#include <memory_resource>
#include <iostream>
#include <cstddef>
class logging_memory_resource : public std::pmr::memory_resource {
public:
explicit logging_memory_resource(std::pmr::memory_resource* upstream
= std::pmr::get_default_resource())
: upstream_(upstream) {}
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
void* p = upstream_->allocate(bytes, alignment);
std::cout << "[alloc] " << bytes << " bytes, align " << alignment
<< " -> " << p << "\n";
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
std::cout << "[dealloc] " << bytes << " bytes @ " << p << "\n";
upstream_->deallocate(p, bytes, alignment);
}
[[nodiscard]] bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::pmr::memory_resource* upstream_;
};
예제 2: 통계 수집 memory_resource
#include <memory_resource>
#include <atomic>
#include <cstddef>
class stats_memory_resource : public std::pmr::memory_resource {
public:
explicit stats_memory_resource(std::pmr::memory_resource* upstream
= std::pmr::get_default_resource())
: upstream_(upstream) {}
std::size_t allocation_count() const noexcept { return alloc_count_.load(); }
std::size_t total_allocated() const noexcept { return total_allocated_.load(); }
std::size_t peak_allocated() const noexcept { return peak_allocated_.load(); }
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
void* p = upstream_->allocate(bytes, alignment);
alloc_count_.fetch_add(1);
std::size_t prev = total_allocated_.fetch_add(bytes);
std::size_t current = prev + bytes;
for (std::size_t peak = peak_allocated_.load();
current > peak && !peak_allocated_.compare_exchange_weak(peak, current);
peak = peak_allocated_.load()) {}
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
total_allocated_.fetch_sub(bytes);
upstream_->deallocate(p, bytes, alignment);
}
[[nodiscard]] bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::pmr::memory_resource* upstream_;
std::atomic<std::size_t> alloc_count_{0};
std::atomic<std::size_t> total_allocated_{0};
std::atomic<std::size_t> peak_allocated_{0};
};
예제 3: 스레드별 monotonic 풀
#include <memory_resource>
#include <vector>
#include <thread>
thread_local std::pmr::monotonic_buffer_resource* tls_pool = nullptr;
void initThreadPool() {
tls_pool = new std::pmr::monotonic_buffer_resource(
std::pmr::new_delete_resource());
}
void cleanupThreadPool() {
delete tls_pool;
tls_pool = nullptr;
}
std::pmr::memory_resource* getThreadPool() {
if (!tls_pool) initThreadPool();
return tls_pool;
}
void worker(int id) {
std::pmr::vector<int> local_data(getThreadPool());
for (int i = 0; i < 1000; ++i) {
local_data.push_back(i * id);
}
}
예제 4: 고정 크기 블록 풀 (단순 구현)
#include <memory_resource>
#include <vector>
#include <cstddef>
class fixed_block_pool : public std::pmr::memory_resource {
public:
explicit fixed_block_pool(std::size_t block_size, std::size_t block_count = 64)
: block_size_(block_size), blocks_(block_count) {
storage_.resize(block_size * block_count);
for (std::size_t i = 0; i < block_count; ++i) {
free_list_.push_back(storage_.data() + i * block_size);
}
}
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
if (bytes > block_size_) return nullptr;
if (free_list_.empty()) return nullptr;
void* p = free_list_.back();
free_list_.pop_back();
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p >= storage_.data() && p < storage_.data() + storage_.size()) {
free_list_.push_back(static_cast<std::byte*>(p));
}
}
[[nodiscard]] bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::size_t block_size_;
std::vector<std::byte> storage_;
std::vector<std::byte*> blocks_;
std::vector<std::byte*> free_list_;
};
7. pmr 컨테이너 실전 활용
지원되는 pmr 컨테이너
| 컨테이너 | 별칭 | 용도 |
|---|---|---|
| std::pmr::string | vector<char, polymorphic_allocator<char>> | 문자열 |
| std::pmr::vector | vector<T, polymorphic_allocator<T>> | 동적 배열 |
| std::pmr::map | map<K,V,…,polymorphic_allocator<pair<…>>> | 정렬 맵 |
| std::pmr::set | set<T,…,polymorphic_allocator<T>> | 정렬 집합 |
| std::pmr::unordered_map | unordered_map with pmr allocator | 해시 맵 |
| std::pmr::list | list<T, polymorphic_allocator<T>> | 이중 연결 리스트 |
중첩 pmr 컨테이너: map<string, vector<string>>
#include <memory_resource>
#include <vector>
#include <string>
#include <map>
void parseConfig(const char* raw) {
std::pmr::monotonic_buffer_resource pool;
// map의 key, value 모두 pmr::string
// value가 vector<pmr::string>이면 그 안의 string도 pool 사용
std::pmr::map<std::pmr::string, std::pmr::vector<std::pmr::string>, std::less<>>
config(&pool);
config["sections"].push_back("a");
config["sections"].push_back("b");
config["keys"].push_back("x");
// 모든 할당이 pool에서 이루어짐
}
pmr string 주의: std::string과 혼용 금지
// ❌ 위험: std::string과 std::pmr::string 혼용
std::pmr::map<std::pmr::string, std::string, std::less<>> m(&pool);
// value가 std::string이면 기본 할당자 사용 — pool과 무관
// ✅ 올바름: 모두 pmr
std::pmr::map<std::pmr::string, std::pmr::string, std::less<>> m(&pool);
8. 자주 발생하는 에러와 해결법
에러 1: 풀 수명보다 컨테이너가 오래 살 때 (Use-After-Free)
증상: 크래시, 정의되지 않은 동작, 힙 손상.
원인: memory_resource가 먼저 소멸되었는데, 그 리소스를 사용하는 컨테이너가 아직 살아 있을 때.
// ❌ 잘못된 코드
std::pmr::vector<int>* createVector() {
std::pmr::monotonic_buffer_resource pool;
return new std::pmr::vector<int>(&pool); // pool은 함수 종료 시 소멸!
}
// 반환된 vector는 이미 해제된 pool을 가리킴 → UB
해결법:
// ✅ 올바른 코드: 풀 수명을 컨테이너보다 길게
std::pmr::monotonic_buffer_resource* pool = new std::pmr::monotonic_buffer_resource;
std::pmr::vector<int>* vec = new std::pmr::vector<int>(pool);
// vec 소멸 전에 pool을 소멸하지 않도록 수명 관리
또는 풀과 컨테이너를 같은 스코프에 두기:
// ✅ 풀과 컨테이너 수명 일치
void process() {
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> vec(&pool);
// ... 사용 ...
} // vec, pool 순서대로 소멸
에러 2: monotonic에서 deallocate 기대
증상: 메모리가 해제되지 않고 계속 쌓임.
원인: monotonic_buffer_resource의 deallocate는 no-op입니다.
// ❌ 잘못된 기대
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> v(&pool);
v.push_back(1);
v.pop_back(); // 내부적으로 deallocate 호출되지만, pool은 해제하지 않음
// release() 호출 전까지 메모리는 풀에 남아 있음
해결법: monotonic은 release()로 전체 리셋하는 용도로만 사용.
// ✅ monotonic: 스코프 끝에 release
{
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> v(&pool);
v.push_back(1);
v.pop_back();
pool.release(); // 이 시점에 풀 전체 리셋
}
에러 3: 서로 다른 리소스에서 할당한 컨테이너끼리 복사/이동
증상: 크래시 또는 메모리 손상.
// ❌ 위험한 코드
std::pmr::monotonic_buffer_resource pool1, pool2;
std::pmr::vector<int> a(&pool1);
a.push_back(42);
std::pmr::vector<int> b(&pool2);
b = a; // a의 allocator가 b에 복사됨. b는 pool2를 쓰지만 데이터는 pool1에서 옴
// b 소멸 시 pool2.deallocate에 pool1의 포인터 전달 → UB
해결법: 같은 리소스를 쓰는 컨테이너끼리만 복사/이동.
// ✅ 같은 풀 사용
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> a(&pool);
std::pmr::vector<int> b(&pool);
a.push_back(42);
b = a; // 둘 다 pool 사용 → 안전
에러 4: 스택 버퍼 크기 부족
증상: monotonic이 업스트림(힙)에서 추가 할당.
// ❌ 버퍼가 너무 작을 수 있음
char buffer[256];
std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
std::pmr::vector<int> v(&pool);
for (int i = 0; i < 1000; ++i) v.push_back(i); // 256바이트 초과 → 힙 사용
해결법: 버퍼 크기를 넉넉히 잡기.
// ✅ 넉넉한 버퍼
std::array<std::byte, 65536> buffer;
std::pmr::monotonic_buffer_resource pool{
buffer.data(), buffer.size(),
std::pmr::new_delete_resource()
};
에러 5: pool_options 부적절한 설정
증상: synchronized_pool_resource에서 메모리 낭비 또는 할당 실패.
해결법: 실제 사용하는 최대 블록 크기에 맞춰 설정.
// ✅ 사용 패턴에 맞는 설정
std::pmr::pool_options opts;
opts.largest_required_pool_block = 64; // 주로 64바이트 이하 객체
opts.max_blocks_per_chunk = 128;
std::pmr::synchronized_pool_resource pool{opts};
에러 6: alignment 무시
증상: 특정 플랫폼에서 크래시 또는 SIGBUS.
// ❌ 잘못된 구현 (alignment 무시)
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
return upstream_->allocate(bytes, 1); // alignment 무시!
}
해결법: 항상 요청된 alignment를 전달.
// ✅ alignment 준수
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
return upstream_->allocate(bytes, alignment);
}
에러 7: 스레드 안전성 오해
증상: 멀티스레드에서 크래시 또는 데이터 손상.
// ❌ 위험: 여러 스레드가 같은 unsynchronized_pool 공유
std::pmr::unsynchronized_pool_resource pool;
std::thread t1([&] { std::pmr::vector<int> v(&pool); /* ... */ });
std::thread t2([&] { std::pmr::vector<int> v(&pool); /* ... */ });
해결법: 멀티스레드에서 공유할 풀은 synchronized_pool_resource 사용.
// ✅ synchronized_pool_resource (멀티스레드 안전)
std::pmr::synchronized_pool_resource pool;
9. 베스트 프랙티스
1. 풀 선택 가이드
| 패턴 | 권장 | 이유 |
|---|---|---|
| 프레임/요청 단위, 일괄 해제 | monotonic | deallocate 없음, release()로 리셋 |
| 개별 객체 반복 할당/해제 | pool_resource | 블록 재사용 |
| 스레드별 독립 할당 | 스레드별 monotonic | 락 경합 제거 |
| 디버깅/프로파일링 | stats_memory_resource 래핑 | 할당 횟수·피크 확인 |
2. 풀 수명 규칙
항상: 풀 수명 >= 이를 사용하는 모든 컨테이너 수명
3. pmr 컨테이너 내부 요소도 pmr로
// ✅ map의 key, value 모두 pmr 타입
std::pmr::map<std::pmr::string, std::pmr::vector<int>, std::less<>> m(&pool);
4. 프로파일링 후 적용
// 1단계: 프로파일링으로 병목 확인
// 2단계: 해당 함수/스코프에만 풀 도입
// 3단계: 성능 측정 후 확대 적용
5. 디버그 빌드에서 통계 리소스 사용
#ifdef NDEBUG
std::pmr::memory_resource* resource = std::pmr::get_default_resource();
#else
static stats_memory_resource stats{std::pmr::get_default_resource()};
std::pmr::memory_resource* resource = &stats;
#endif
std::pmr::vector<int> data(resource);
// ... 처리 ...
#ifndef NDEBUG
std::cout << "Allocations: " << stats.allocation_count()
<< ", Peak: " << stats.peak_allocated() << " bytes\n";
#endif
10. 성능 벤치마크
벤치마크 1: 기본 할당 vs monotonic vs pool
#include <chrono>
#include <memory_resource>
#include <vector>
#include <iostream>
void benchmarkAllocators() {
constexpr size_t N = 100000;
constexpr size_t elem_size = 32;
// 1. 기본 힙 할당
auto t1 = std::chrono::high_resolution_clock::now();
{
std::vector<int> v;
v.reserve(N);
for (size_t i = 0; i < N; ++i) v.push_back(static_cast<int>(i));
}
auto t2 = std::chrono::high_resolution_clock::now();
// 2. monotonic_buffer_resource
std::vector<std::byte> buffer(N * elem_size * 2);
std::pmr::monotonic_buffer_resource mono{
buffer.data(), buffer.size(),
std::pmr::new_delete_resource()
};
auto t3 = std::chrono::high_resolution_clock::now();
{
std::pmr::vector<int> v(&mono);
v.reserve(N);
for (size_t i = 0; i < N; ++i) v.push_back(static_cast<int>(i));
}
auto t4 = std::chrono::high_resolution_clock::now();
// 3. synchronized_pool_resource
std::pmr::synchronized_pool_resource pool;
auto t5 = std::chrono::high_resolution_clock::now();
{
std::pmr::vector<int> v(&pool);
v.reserve(N);
for (size_t i = 0; i < N; ++i) v.push_back(static_cast<int>(i));
}
auto t6 = std::chrono::high_resolution_clock::now();
using namespace std::chrono;
auto d_default = duration_cast<microseconds>(t2 - t1).count();
auto d_mono = duration_cast<microseconds>(t4 - t3).count();
auto d_pool = duration_cast<microseconds>(t6 - t5).count();
std::cout << "Default: " << d_default << " μs\n";
std::cout << "Monotonic: " << d_mono << " μs (" << (double)d_default/d_mono << "x)\n";
std::cout << "Pool: " << d_pool << " μs (" << (double)d_default/d_pool << "x)\n";
}
예상 결과:
| 할당자 | 10만 push_back (μs) | 상대 속도 |
|---|---|---|
| 기본 vector | 2000~5000 | 1x |
| monotonic | 500~1500 | 2~4x |
| synchronized_pool | 800~2000 | 1.5~3x |
벤치마크 요약 표
| 시나리오 | 기본 힙 | monotonic | pool_resource | 비고 |
|---|---|---|---|---|
| 순차 push_back (해제 없음) | 1x | 2~4x | 1.5~3x | monotonic 유리 |
| 반복 할당/해제 (고정 크기) | 1x | 부적합 | 2~5x | pool 유리 |
| 스레드 경합 많음 | 1x | 스레드별 사용 시 유리 | 동기화 오버헤드 | 스레드별 monotonic |
11. 프로덕션 패턴
패턴 1: 게임 프레임 풀
void gameLoop() {
std::array<std::byte, 1024*1024> frame_buffer;
while (running) {
std::pmr::monotonic_buffer_resource frame_pool{
frame_buffer.data(), frame_buffer.size(),
std::pmr::new_delete_resource()
};
std::pmr::vector<Entity> entities(&frame_pool);
std::pmr::vector<Component> components(&frame_pool);
// ... 엔티티 생성, 프레임 로직 ...
} // frame_pool, entities 소멸 → 다음 프레임에서 같은 buffer 재사용
}
패턴 2: HTTP 요청 스코프
void handleRequest(const Request& req) {
std::pmr::monotonic_buffer_resource request_pool(
std::pmr::new_delete_resource());
std::pmr::vector<std::pmr::string> path_segments(&request_pool);
std::pmr::map<std::pmr::string, std::pmr::string, std::less<>>
headers(&request_pool);
parsePath(req.uri, path_segments);
parseHeaders(req.raw_headers, headers);
Response response = processRequest(path_segments, headers);
sendResponse(response);
} // request_pool 소멸 → 모든 할당 해제
패턴 3: 스레드 풀 워커별 풀
class Worker {
std::pmr::monotonic_buffer_resource worker_pool_;
std::pmr::vector<Task> local_queue_{&worker_pool_};
public:
Worker() : worker_pool_(std::pmr::new_delete_resource()) {}
void process(Task t) {
worker_pool_.release();
local_queue_.clear();
// t 처리 시 local_queue_ 등에서 할당
}
};
패턴 4: 계층적 리소스 (업스트림)
std::pmr::synchronized_pool_resource global_pool;
std::pmr::monotonic_buffer_resource thread_pool{&global_pool};
std::pmr::monotonic_buffer_resource frame_pool{&thread_pool};
std::pmr::vector<int> frame_data(&frame_pool);
// frame_pool 부족 → thread_pool → global_pool
패턴 5: 구현 체크리스트
- 풀 수명이 이를 사용하는 모든 컨테이너보다 길다
- monotonic 사용 시
release()호출 시점 명확히 (프레임/요청 끝) - 서로 다른 풀을 쓰는 pmr 컨테이너 간 복사/대입 금지
- 스택 버퍼 사용 시 크기 여유 있게
- pool_options는 실제 사용 패턴에 맞게
- 멀티스레드 시
synchronized_pool_resource또는 스레드별 풀
12. 정리
| 주제 | 요약 |
|---|---|
| 메모리 풀 | 큰 블록 한 번 할당 후 슬롯 나눠 쓰기 — 할당 횟수·단편화 감소 |
| std::pmr | memory_resource + polymorphic_allocator로 컨테이너에 풀 주입 |
| monotonic | 순차 할당·리셋만 — 프레임/요청 스코프에 적합 |
| pool_resource | 고정 크기 블록 재사용 — 일반 객체 풀에 적합 |
| 커스텀 리소스 | memory_resource 상속으로 로깅, 통계, 스레드별 풀 구현 |
핵심 원칙:
- 풀 수명 > 컨테이너 수명
- monotonic은
release()로만 해제 - 서로 다른 풀의 컨테이너 간 복사/대입 주의
- 프로파일링으로 실제 이득 확인 후 적용
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 캐시 효율적인 코드: 데이터 지향 설계 가이드
- C++ SIMD와 병렬화: std::execution과 인트린직 가이드
- C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
이 글에서 다루는 키워드 (관련 검색어)
std::pmr, polymorphic memory resource, 메모리 풀, monotonic_buffer_resource, pool_resource, polymorphic_allocator, pmr 컨테이너 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 프로파일러에서 malloc/free가 상위를 차지할 때, 게임 프레임·HTTP 요청처럼 짧은 수명의 객체를 많이 할당할 때, 노드 기반 자료구조에서 고정 크기 할당이 반복될 때 적용하면 됩니다.
Q. monotonic과 pool_resource 중 뭘 써야 하나요?
A. 프레임/요청처럼 “한 번에 할당하고 끝에 한 번에 해제”하는 패턴이면 monotonic. 개별 객체를 반복 할당/해제하는 패턴이면 pool_resource가 적합합니다.
Q. 선행으로 읽으면 좋은 글은?
A. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다. #39-1 캐시·데이터 지향 설계를 먼저 읽으면 좋습니다.
Q. 더 깊이 공부하려면?
A. cppreference std::pmr와 P0339R6 문서를 참고하세요.
한 줄 요약: std::pmr로 메모리 풀·단편화를 제어할 수 있습니다. 다음으로 SIMD·인트린직(#39-3)를 읽어보면 좋습니다.
이전 글: 고성능 C++ #39-1: 캐시·데이터 지향 설계
다음 글: 고성능 C++ #39-3: SIMD와 병렬화
관련 글
- C++ 현대적 메모리 관리: 커스텀 알로케이터 제작과 std::pmr 가이드
- C++ 캐시 효율적인 코드: 데이터 지향 설계 가이드
- C++ std::chrono 완벽 가이드 | duration·time_point·클럭·시간 측정 실전 활용
- C++ SIMD와 병렬화: std::execution과 인트린직 가이드
- C++ 메모리 관리 완벽 가이드 | 할당자·풀·아레나·프로덕션 패턴 [#55-5]