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 起,constructdestroy 成员函数已废弃。C++23 标准中已完全移除。使用 std::construct_atstd::destroy_at 代替。

1
2
3
4
5
6
7
8
9
10
11
12
// 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 控制:

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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 从预分配的大块内存中线性分配,适合高性能场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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_resourceunsynchronized_pool_resourcenew_delete_resource()

优势

  • 运行时切换策略,无需模板膨胀。
  • 池化减少分配开销。
  • 线程安全选项。

5.1 示例:Monotonic Buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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

1
2
3
4
5
6
7
8
#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 优化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#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 默认):
pmr-bench

使用 jemalloc + ‘-O’:
这是使用 jemalloc 编译选项后的运行结果
jedis

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’ 编译选项后的运行结果
jedis

典型结果(相对性能):

  • 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 更慢

关键洞察

  1. 测量优先:不要假设 PMR 一定更快,必须在实际场景中测试。
  2. 场景依赖:PMR 的优势取决于数据量、生命周期和分配模式。
  3. 简单第一:如果默认 allocator 满足需求,不要为了"优化"而引入 PMR。
  4. 实际需求:PMR 的主要优势是灵活性和多态性,而不是绝对性能。

7. 与智能指针配合

7.1 std::allocate_shared

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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. 最佳实践

  1. 默认就好:99% 场景用 std::allocator。别为了"优化"引入复杂。
  2. PMR 入门:从小 buffer 开始测试。monotonic 零成本抽象。
  3. 测量优先:用基准测试验证优化效果,别凭感觉。
  4. 简单第一:能用 PMR 就别自己写 allocator。
  5. 线程安全:多线程用 synchronized_pool_resource,单线程用 unsynchronized_pool_resource

9. C++20/23 新特性

9.1 C++20 增强

  • std::pmr::polymorphic_allocator 支持 std::construct_atstd::destroy_at
  • 更好的异常安全保证。
  • 改进的 allocate_bytesdeallocate_bytes
  • C++23 标准中,allocator 接口已简化,不再要求实现 constructdestroy 成员函数。

9.2 C++23 增强

  • std::printstd::println 支持 PMR 容器的格式化输出。
  • std::pmr::get_default_resource() 改进,线程安全的默认资源管理。
  • 扩展了 PMR 容器支持:dequeforward_listlistmapsetunordered_map 等。

10. 陷阱与调试

  • 相等性:Stateful allocator 的不同实例必须能够通过 operator== 进行比较,否则容器的行为是未定义的。
  • 异常安全allocatestd::bad_alloc
  • 对齐:C++17 起,allocate 保证 over-aligned。
  • 调试:用 AddressSanitizer clang++ -fsanitize=address

10.1 常见错误

错误 1:Stateful allocator 拷贝问题

1
2
3
4
// 错误:不同实例的 allocator 不相等
std::vector<int, PoolAllocator<int>> vec1{PoolAllocator<int>(100)};
std::vector<int, PoolAllocator<int>> vec2{PoolAllocator<int>(200)};
vec1 = vec2; // 未定义行为!

正确做法:确保 allocator 相等或使用 propagation traits。

1
2
3
4
5
6
template <typename T>
class PoolAllocator {
public:
using propagate_on_container_copy_assignment = std::true_type;
// ...
};

错误 2:忘记实现 operator==

1
2
3
4
5
// 错误:stateful allocator 没有 operator==
template <typename T>
class MyAllocator {
// 缺少 operator==
};

正确做法:实现 operator== 或设置 is_always_equal。

1
2
3
4
5
6
7
template <typename T>
class MyAllocator {
public:
using is_always_equal = std::true_type; // 如果无状态
// 或
bool operator==(const MyAllocator&) const noexcept { return true; }
};

错误 3:对齐问题

1
2
3
4
5
6
7
// 错误:不支持 over-aligned 类型
template <typename T>
class BadAllocator {
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T))); // 忽略对齐
}
};

正确做法:使用 aligned new。

1
2
3
4
5
6
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,演进巨大。但记住:实用主义。解决真问题,别追虚幻性能。

核心要点

  1. 默认就好:99% 场景用 std::allocator
  2. PMR 是王道:需要优化时,优先用 PMR,别自己写 allocator。
  3. 测量优先:用基准测试验证优化效果。
  4. 简单第一:过度设计 allocator 往往适得其反。
  5. 向后兼容:Never break userspace,确保 allocator 行为一致。