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 代替。

// 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_resourceunsynchronized_pool_resourcenew_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 默认):
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

#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 拷贝问题

// 错误:不同实例的 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,演进巨大。但记住:实用主义。解决真问题,别追虚幻性能。

核心要点

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