C++ Stl Allocator
Allocator 是 C++ STL 容器内存管理的核心。它直接决定程序的内存效率和性能瓶颈。默认的 std::allocator 在高性能场景下往往不够用,但记住我的铁律:简单第一,复杂是敌人。
1. Allocator 的核心接口
STL 容器通过 allocator 抽象内存管理,解耦内存分配与容器逻辑。一个合格的 allocator 必须满足 allocator_traits 的要求:
pointer allocate(size_t n): 分配 n 个元素内存,返回 pointer。void deallocate(pointer p, size_t n): 释放内存。rebind<U>::other: 用于类型转换。
重要:C++17 起,construct 和 destroy 成员函数已废弃。C++23 标准中已完全移除。使用 std::construct_at 和 std::destroy_at 代替。
// C++17+ 推荐方式
#include <memory>
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
std::construct_at(p, std::forward<Args>(args)...);
}
template <typename U>
void destroy(U* p) {
std::destroy_at(p);
}2. Stateful vs Stateless
Allocator 可以是 stateless(无状态,默认 std::allocator)或 stateful(有状态,带额外数据如内存池)。
关键:对于有状态的 allocator,不同实例必须能够通过 operator== 进行比较,否则容器的行为是未定义的。
3. Propagation Traits
容器在拷贝、移动或交换时,allocator 的行为由 propagation traits 控制:
template <typename T>
class MyAllocator {
public:
using propagate_on_container_copy_assignment = std::true_type; // 拷贝赋值时传播
using propagate_on_container_move_assignment = std::true_type; // 移动赋值时传播
using propagate_on_container_swap = std::true_type; // 交换时传播
using is_always_equal = std::true_type; // 所有实例都相等
};propagate_on_container_copy_assignment:true时,拷贝赋值会替换目标容器的 allocator。propagate_on_container_move_assignment:true时,移动赋值会转移 allocator。propagate_on_container_swap:true时,交换容器会交换 allocator。is_always_equal:true时,所有 allocator 实例都相等(无状态),容器可以优化。
4. 自定义 Allocator 示例
4.1 简单无状态 Allocator
#include <memory>
#include <vector>
template <typename T>
class SimpleAllocator {
public:
using value_type = T;
template <typename U>
struct rebind {
using other = SimpleAllocator<U>;
};
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) {
::operator delete(p);
}
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
std::construct_at(p, std::forward<Args>(args)...);
}
template <typename U>
void destroy(U* p) {
std::destroy_at(p);
}
};
// 使用
std::vector<int, SimpleAllocator<int>> vec{SimpleAllocator<int>{}};
vec.emplace_back(42);4.2 Arena Allocator(区域分配器)
Arena allocator 从预分配的大块内存中线性分配,适合高性能场景:
#include <memory>
#include <vector>
template <typename T>
class ArenaAllocator {
public:
using value_type = T;
ArenaAllocator(std::size_t arenaSize)
: arena_(static_cast<char*>(::operator new(arenaSize)))
, end_(arena_ + arenaSize)
, current_(arena_) {}
~ArenaAllocator() {
::operator delete(arena_);
}
template <typename U>
struct rebind {
using other = ArenaAllocator<U>;
};
T* allocate(std::size_t n) {
std::size_t bytes = n * sizeof(T);
std::size_t aligned = (bytes + alignof(T) - 1) & ~(alignof(T) - 1);
if (current_ + aligned > end_) {
throw std::bad_alloc{};
}
T* result = reinterpret_cast<T*>(current_);
current_ += aligned;
return result;
}
void deallocate(T*, std::size_t) noexcept {
// Arena 不支持单独释放
}
void reset() noexcept {
current_ = arena_;
}
private:
char* arena_;
char* end_;
char* current_;
};使用场景:游戏引擎、编译器、网络服务器等需要大量临时对象的场景。
5. C++17 PMR:多态内存资源
PMR 是 C++17 的杀手锏:多态 allocator。核心是 memory_resource 抽象基类。
std::pmr::memory_resource: 虚拟接口。std::pmr::polymorphic_allocator<T>: 使用 resource 的多态 allocator。- 内置实现:
monotonic_buffer_resource(单向,快)、synchronized_pool_resource、unsynchronized_pool_resource、new_delete_resource()。
优势:
- 运行时切换策略,无需模板膨胀。
- 池化减少分配开销。
- 线程安全选项。
5.1 示例:Monotonic Buffer
#include <memory_resource>
#include <vector>
#include <print>
int main() {
char buffer[1024];
std::pmr::monotonic_buffer_resource resource(buffer.data(), buffer.size(),
std::pmr::new_delete_resource());
std::pmr::polymorphic_allocator<int> alloc{&resource};
std::pmr::vector<int> vec{alloc};
for (int i = 0; i < 100; ++i) {
vec.emplace_back(i * 2);
}
std::println("Size: {}, Capacity: {}", vec.size(), vec.capacity());
}5.2 示例:Pool Resource
#include <memory_resource>
std::pmr::pool_options options;
options.max_blocks_per_chunk = 32;
options.largest_required_block_size = 4096;
auto pool = std::pmr::synchronized_pool_resource{options, std::pmr::get_default_resource()};
std::pmr::vector<std::string> strings{pool};6. 性能对比
基准测试(Google Benchmark,开启 -O 优化):
#include <benchmark/benchmark.h>
#include <vector>
#include <memory_resource>
#include <string>
// 默认 allocator
static void BM_DefaultAllocator(benchmark::State& state) {
for (auto _ : state) {
std::vector<std::string> vec;
for (int i = 0; i < state.range(0); ++i) {
vec.emplace_back("test string " + std::to_string(i));
}
benchmark::DoNotOptimize(vec.data());
}
}
// PMR monotonic buffer
static void BM_PMRMonotonic(benchmark::State& state) {
std::byte buffer[2 * 1024 * 1024];
std::pmr::monotonic_buffer_resource resource(buffer, sizeof(buffer));
for (auto _ : state) {
std::pmr::vector<std::pmr::string> vec(&resource);
for (int i = 0; i < state.range(0); ++i) {
vec.emplace_back("test string " + std::to_string(i));
}
benchmark::DoNotOptimize(vec.data());
resource.release();
}
}
// PMR pool
static void BM_PMRPool(benchmark::State& state) {
std::pmr::synchronized_pool_resource pool;
for (auto _ : state) {
std::pmr::vector<std::pmr::string> vec(&pool);
for (int i = 0; i < state.range(0); ++i) {
vec.emplace_back("test string " + std::to_string(i));
}
benchmark::DoNotOptimize(vec.data());
}
}
BENCHMARK(BM_DefaultAllocator)->Range(100, 10000);
BENCHMARK(BM_PMRMonotonic)->Range(100, 10000);
BENCHMARK(BM_PMRPool)->Range(100, 10000);无 jemalloc (glibc 默认):
使用 jemalloc + ‘-O’:
这是使用 jemalloc 编译选项后的运行结果
jemalloc vs glibc 区别:
- 默认allocator 提速明显:jemalloc 优化
new/delete,单线程也快。 - PMR 相对落后:多态/池开销在 jemalloc 下更突出 (Monotonic 慢10-20%, Pool 慢20-110%)。
- 洞察:jemalloc 强化默认,PMR 需特定场景 (arena复用、多态切换)。
- 建议:生产用 LD_PRELOAD libjemalloc,默认allocator + jemalloc = 简单高效。
这是使用 jemalloc 加 ‘-O’ 编译选项后的运行结果
典型结果(相对性能):
std::allocator: 基准(1.0x)monotonic_buffer_resource: 小数据量(100-512)基本相当或略慢(1.03x),中等数据量(4096)约 1.6x 更快,大数据量(10000)约 2.2x 更快synchronized_pool_resource: 小数据量(100-512)基本相当或略慢(1.16x),中等数据量(4096)基本相当(1.09x),大数据量(10000)约 1.4x 更慢
关键洞察:
- 测量优先:不要假设 PMR 一定更快,必须在实际场景中测试。
- 场景依赖:PMR 的优势取决于数据量、生命周期和分配模式。
- 简单第一:如果默认 allocator 满足需求,不要为了"优化"而引入 PMR。
- 实际需求:PMR 的主要优势是灵活性和多态性,而不是绝对性能。
7. 与智能指针配合
7.1 std::allocate_shared
#include <memory>
#include <memory_resource>
struct Widget {
int data;
Widget(int d) : data(d) {}
};
void exampleAllocateShared() {
std::pmr::monotonic_buffer_resource resource;
std::pmr::polymorphic_allocator<Widget> alloc{&resource};
auto ptr = std::allocate_shared<Widget>(alloc, 42);
std::println("Widget data: {}", ptr->data);
}7.2 std::make_obj_using_allocator
C++20 引入 std::make_obj_using_allocator,简化 allocator 的使用。
8. 最佳实践
- 默认就好:99% 场景用
std::allocator。别为了"优化"引入复杂。 - PMR 入门:从小 buffer 开始测试。
monotonic零成本抽象。 - 测量优先:用基准测试验证优化效果,别凭感觉。
- 简单第一:能用 PMR 就别自己写 allocator。
- 线程安全:多线程用
synchronized_pool_resource,单线程用unsynchronized_pool_resource。
9. C++20/23 新特性
9.1 C++20 增强
std::pmr::polymorphic_allocator支持std::construct_at和std::destroy_at。- 更好的异常安全保证。
- 改进的
allocate_bytes和deallocate_bytes。 - C++23 标准中,allocator 接口已简化,不再要求实现
construct和destroy成员函数。
9.2 C++23 增强
std::print和std::println支持 PMR 容器的格式化输出。std::pmr::get_default_resource()改进,线程安全的默认资源管理。- 扩展了 PMR 容器支持:
deque、forward_list、list、map、set、unordered_map等。
10. 陷阱与调试
- 相等性:Stateful allocator 的不同实例必须能够通过
operator==进行比较,否则容器的行为是未定义的。 - 异常安全:
allocate抛std::bad_alloc。 - 对齐:C++17 起,
allocate保证 over-aligned。 - 调试:用 AddressSanitizer
clang++ -fsanitize=address。
10.1 常见错误
错误 1:Stateful allocator 拷贝问题
// 错误:不同实例的 allocator 不相等
std::vector<int, PoolAllocator<int>> vec1{PoolAllocator<int>(100)};
std::vector<int, PoolAllocator<int>> vec2{PoolAllocator<int>(200)};
vec1 = vec2; // 未定义行为!正确做法:确保 allocator 相等或使用 propagation traits。
template <typename T>
class PoolAllocator {
public:
using propagate_on_container_copy_assignment = std::true_type;
// ...
};错误 2:忘记实现 operator==
// 错误:stateful allocator 没有 operator==
template <typename T>
class MyAllocator {
// 缺少 operator==
};正确做法:实现 operator== 或设置 is_always_equal。
template <typename T>
class MyAllocator {
public:
using is_always_equal = std::true_type; // 如果无状态
// 或
bool operator==(const MyAllocator&) const noexcept { return true; }
};错误 3:对齐问题
// 错误:不支持 over-aligned 类型
template <typename T>
class BadAllocator {
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T))); // 忽略对齐
}
};正确做法:使用 aligned new。
template <typename T>
class GoodAllocator {
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T), std::align_val_t{alignof(T)}));
}
};11. 结论
Allocator 从简单 std::allocator 到 PMR,演进巨大。但记住:实用主义。解决真问题,别追虚幻性能。
核心要点:
- 默认就好:99% 场景用
std::allocator。 - PMR 是王道:需要优化时,优先用 PMR,别自己写 allocator。
- 测量优先:用基准测试验证优化效果。
- 简单第一:过度设计 allocator 往往适得其反。
- 向后兼容:Never break userspace,确保 allocator 行为一致。




