编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
        • 1. 案例引入
          • 1.1 游戏引擎的内存碎片风暴
          • 1.2 PMR 的一次性释放惊人行为
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 分配器的四层金字塔
          • 2.2 为何这么切
        • 3. std::allocator 的默认实现
          • 3.1 allocate 与 deallocate 的最小接口
          • 3.2 rebind 的类型转换魔术
          • 3.3 为什么 std::allocator 是无状态的
          • 3.4 构造与析构分离
        • 4. 自定义分配器的正确姿势
          • 4.1 最小兼容的分配器实现
          • 4.2 池分配器的实战
          • 4.3 与容器的集成方式
          • 4.4 分配器传播规则
          • 4.5 池分配器性能对比
          • 4.6 分配器传播与 scoped_allocator 的完整推演
        • 5. PMR 与多态内存资源
          • 5.1 从静态多态到运行时多态
          • 5.2 memory_resource 的虚函数接口
          • 5.3 pmr::vector 的类型别名
          • 5.4 运行时切换分配器的能力
        • 6. 三种标准 memory_resource
          • 6.1 newdeleteresource —— 默认兜底
          • 6.2 monotonic_buffer —— 预分配大块
          • 6.3 unsynchronized_pool —— 线程本地池
          • 6.4 benchmark 性能对比
        • 7. scoped_allocator 的嵌套传播
          • 7.1 嵌套容器分配器难题
          • 7.2 scopedallocatoradaptor 的递推传播
          • 7.3 什么时候需要 scoped_allocator
        • 8. 分配器与异常安全的共生
          • 8.1 allocate 失败的标准行为
          • 8.2 分配失败的回滚策略
        • 9. 各场景分配器选型
          • 9.1 高频交易 / 游戏 / 嵌入式 场景对比
          • 9.2 与 jemalloc / tcmalloc 的分工
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次 pmr::vector::push_back 的完整分配链路
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

Allocator分配器机制

# 39.Allocator分配器机制

# 目录介绍

  • 1. 案例引入
    • 1.1 游戏引擎的内存碎片风暴
    • 1.2 PMR 的一次性释放惊人行为
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 分配器的四层金字塔
    • 2.2 为何这么切
  • 3. std::allocator 的默认实现
    • 3.1 allocate 与 deallocate 的最小接口
    • 3.2 rebind 的类型转换魔术
    • 3.3 为什么 std::allocator 是无状态的
    • 3.4 构造与析构的分离设计
  • 4. 自定义分配器的正确姿势
    • 4.1 最小兼容的分配器实现
    • 4.2 池分配器的实战
    • 4.3 与容器的集成方式
    • 4.4 分配器在容器间的传播规则
  • 5. PMR 与多态内存资源
    • 5.1 从静态多态到运行时多态
    • 5.2 memory_resource 的虚函数接口
    • 5.3 pmr::vector 的类型别名
    • 5.4 运行时切换分配器的能力
  • 6. 三种标准 memory_resource
    • 6.1 new_delete_resource —— 默认兜底
    • 6.2 monotonic_buffer —— 预分配大块
    • 6.3 unsynchronized_pool —— 线程本地池
    • 6.4 benchmark 对比
  • 7. scoped_allocator 的嵌套传播
    • 7.1 嵌套容器的分配器难题
    • 7.2 scoped_allocator_adaptor 的递推传播
    • 7.3 什么时候需要 scoped_allocator
  • 8. 分配器与异常安全的共生
    • 8.1 allocate 失败的标准行为
    • 8.2 容器在分配失败时的回滚策略
  • 9. 各场景分配器选型
    • 9.1 高频交易 / 游戏 / 嵌入式 场景对比
    • 9.2 与 jemalloc / tcmalloc 的分工
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次 pmr::vector::push_back 的完整分配链路
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 游戏引擎的内存碎片风暴

某游戏引擎的粒子系统——每帧生成数千个粒子,每个粒子是一个 std::vector 中的元素。用默认的 std::allocator——每帧数千次 new/delete。上线一个月后出现周期性的帧率掉帧——30ms 的尖峰每隔约 20 秒出现一次:

// ====== 事故代码 V1:默认分配器导致碎片化 ======
struct Particle { glm::vec3 pos, vel; float life; /* ~40 字节 */ };
std::vector<Particle> particles;

void update_frame() {
    particles.clear();     // ① 析构——但不释放内存 (capacity 保留)
    particles.reserve(5000);
    for (int i = 0; i < 5000; ++i) {
        particles.emplace_back(...); // ② 无需分配——capacity 足够 ✅
    }
}
// 这里没问题——vector 的 capacity 避免了每帧分配

// 但场景加载时:
std::vector<std::vector<Particle>> scenes(100);
for (auto& scene : scenes) {
    scene.resize(5000);  // ① 5000 次 operator new(每个 Particle 约 40B)
}
// 100 个场景 × 5000 次分配 = 500000 次小分配 → glibc malloc 碎片化
// → 后续的分配变慢——碎片越多,查找 free list 越慢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

修复——用自定义池分配器免除每元素分配:一个场景的 5000 个 Particle 从同一块预分配池中获取——零碎片。

# 1.2 PMR 的一次性释放惊人行为

团队改用 std::pmr::monotonic_buffer_resource——把场景加载内存全部在预分配池中完成。但发现场景切换时所有 PMR 对象全部失效:

// ====== 事故代码 V2:monotonic_buffer 的全局释放 ======
std::array<std::byte, 50'000'000> buffer;
std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());

std::pmr::vector<std::pmr::string> names(&pool);
names.emplace_back("player_1");  // string 的字符内存在 pool 中
names.emplace_back("npc_42");

// 场景结束——希望释放这 50MB
pool.release();  // ⚠️ 一次性释放所有内存——names 和里面的所有 string 全部失效!
// names.size() 仍然是 2——但元素指向的是 pool 中已被回收的内存 → SIGSEGV
1
2
3
4
5
6
7
8
9
10
11

根因:monotonic_buffer_resource 不跟踪单个分配——它只记住「分配了多少」。release() 把水位线重置为 0——所有从它分配的对象全部失效。它不是 GC——没有「逐个释放」的能力——只有「全局重置」。

# 1.3 七个待解疑问

① std::allocator 内部做了什么? allocate/deallocate/rebind 是什么?       → 第 3 章
② 自定义分配器怎么实现? 最小的兼容接口是什么?                              → 第 4 章
③ PMR 和传统的模板分配器有什么区别? 什么时候该用 PMR?                       → 第 5 章
④ monotonic_buffer / unsynchronized_pool 分别适用于什么场景?               → 第 6 章
⑤ 嵌套容器(string in vector) 的分配器如何传播? scoped_allocator 是什么?    → 第 7 章
⑥ 分配失败时的容器行为是什么? 强异常安全怎么保?                             → 第 8 章
⑦ 分配器和 jemalloc / tcmalloc 是什么关系? 一个替换分配器策略够吗?           → 第 9 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 分配器的四层金字塔

┌──────────────────────────────────────────────────────────┐
│                   分配器四层金字塔                          │
│                                                           │
│  ┌──────────────────────────────────────────────────┐    │
│  │ ④ PMR memory_resource (运行时多态)                 │    │
│  │    切换分配器不改变容器类型——零代码重编译            │    │
│  ├──────────────────────────────────────────────────┤    │
│  │ ③ 自定义 allocator (编译期模板参数)                │    │
│  │    池分配器 / 共享内存分配器 / 调试分配器            │    │
│  ├──────────────────────────────────────────────────┤    │
│  │ ② std::allocator (默认——静态分发到 ::new/delete)   │    │
│  ├──────────────────────────────────────────────────┤    │
│  │ ① operator new / delete (底层——通常是 malloc/free) │    │
│  └──────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.2 为何这么切

疑惑:为什么 C++17 引入 PMR——之前的模板分配器不够用吗?

论证:

  1. 模板分配器导致类型改变——vector<int, MyAlloc> 和 vector<int> 是不同的类型。 不能用同一个函数接口接受两种分配器的 vector。如果你想在运行时切换分配器——模板方案做不到。
  2. PMR 把分配器做成运行时多态——pmr::vector<int> 只有一个类型。 传入不同的 memory_resource* 即可切换底层策略——不需要重编译。
  3. 传统分配器要求 rebind——std::allocator<T> 的内部需要用到 std::allocator<U>(如 list 的节点分配)。这个转换在模板分配器里需要显式声明 rebind——PMR 中由虚函数自动处理。

结论:模板分配器是「编译期选择」,PMR 是「运行期选择」。 如果需要同一个类型但底层分配策略可变——用 PMR。如果不需要——模板分配器更小更快(无虚函数开销)。


# 3. std::allocator 的默认实现

# 3.1 allocate 与 deallocate 的最小接口

// libstdc++ 的 std::allocator 简化版
template <typename T>
class allocator {
public:
    using value_type = T;

    T* allocate(size_t n) {
        // ① 直接调 operator new ——路径 → malloc → brk/mmap
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }

    void deallocate(T* p, size_t n) {
        // ② 不调用析构——只释放内存
        ::operator delete(p);
        // 注意:n 参数在 C++17 之前是必需的(分配器需要知道大小释放)
        // C++17 之后变成可选
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关键:allocate 不构造对象,deallocate 不析构对象。构造和析构是容器的职责。分配器只管「裸内存」。

# 3.2 rebind 的类型转换魔术

疑惑:如果容器用 allocator<T>——内部需要 allocator<ListNode> 怎么办?

论证——rebind 机制:

// std::list<int, std::allocator<int>> 的内部:
//  需要分配的不是 int——而是 _List_node<int>(包含 prev/next+data)
//  rebind 把 allocator<int> 转换为 allocator<_List_node<int>>

template <typename T>
class allocator {
    template <typename U>
    struct rebind {
        using other = allocator<U>;  // 同一个分配策略——用于不同类型
    };
};

// 容器内部用法:
using NodeAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<Node>;
NodeAlloc node_alloc(original_alloc);  // 从 T 的分配器创建 Node 的分配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

rebind 保证了同一个分配策略可以用于不同大小的对象——这对节点型容器(list、map、unordered_map)至关重要——它们分配的是内部节点类型,不是用户看到的 value_type。

# 3.3 为什么 std::allocator 是无状态的

static_assert(sizeof(std::allocator<int>) == 1);  // 空类——EBO 优化为 0

// vector 继承 allocator(EBO)——不增加 sizeof
// vector<int> 的 sizeof = 24(3 指针),不是 32(如果有 allocator 成员)
1
2
3
4

无状态的好处:默认构造、拷贝、赋值全是平凡操作。容器的 move 构造可以不搬移分配器。

# 3.4 构造与析构分离

C++17 弃用、C++20 移除的 construct/destroy 成员——说明构造/析构不是分配器的职责:

// ❌ C++17 前——分配器负责构造
alloc.construct(ptr, args...);  // placement new——已弃用

// ✅ C++17 之后——容器直接调 placement new
::new (ptr) T(std::forward<Args>(args)...);
1
2
3
4
5

# 4. 自定义分配器的正确姿势

# 4.1 最小兼容的分配器实现

template <typename T>
class PoolAllocator {
    std::vector<std::aligned_storage_t<sizeof(T), alignof(T)>> pool_;
    size_t next_ = 0;
public:
    using value_type = T;

    T* allocate(size_t n) {
        if (next_ + n > pool_.size()) {
            // 池不够——回退到默认 new
            return static_cast<T*>(::operator new(n * sizeof(T)));
        }
        T* p = reinterpret_cast<T*>(&pool_[next_]);
        next_ += n;
        return p;
    }

    void deallocate(T* p, size_t n) noexcept {
        // 池中分配的不单独释放——批量回收
        // 回退到 new 的用 delete 释放
        if (p < reinterpret_cast<T*>(&pool_[0]) ||
            p >= reinterpret_cast<T*>(&pool_[pool_.size()])) {
            ::operator delete(p);
        }
    }

    // rebind 用于同策略的类型转换
    template <typename U>
    struct rebind { using other = PoolAllocator<U>; };

    // 必须支持不同分配器的比较——同类型分配器永远相等(池共享)
    bool operator==(const PoolAllocator&) const noexcept { return true; }
    bool operator!=(const PoolAllocator&) const noexcept { return false; }
};
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

# 4.2 池分配器的实战

// 游戏场景:每帧 5000 个粒子——预分配 10000 个槽
PoolAllocator<Particle> particle_alloc;
particle_alloc.reserve(10000);

std::vector<Particle, PoolAllocator<Particle>> particles(particle_alloc);
particles.reserve(5000);
// 所有 push_back —— 直接从池中获取——零 malloc 调用、零碎片
1
2
3
4
5
6
7

# 4.3 与容器的集成方式

// 容器的最后一个模板参数就是分配器
std::vector<int, MyAllocator<int>> v(my_alloc);

// C++17 CTAD 让它更简洁
std::vector v(10, 0, my_alloc);  // 自动推导为 vector<int, MyAllocator<int>>
1
2
3
4
5

# 4.4 分配器传播规则

std::vector<int, MyAlloc> v1(my_alloc);
auto v2 = v1;  // 拷贝——v2 的分配器也是 v1 的分配器?标准:是
auto v3 = std::move(v1);  // 移动——v3 的分配器?标准:也是(move 传播分配器)
1
2
3

# 4.5 池分配器性能对比

疑惑:池分配器真的能快多少?和 monotonic_buffer 哪个更快?

论证——100 万次 vector<int> 的 push_back + clear 循环:

分配器 push_back clear 总时间 碎片 说明
std::allocator 5.2 ms 0.1 ms 5.3 ms 有 operator new→malloc
PoolAllocator(预分配) 1.8 ms 0 ms 1.8 ms 零 直接池中取
monotonic_buffer 1.5 ms —(release) 1.5 ms 零 水位线下分配
unsynchronized_pool 3.1 ms 0.5 ms 3.6 ms 低 线程本地 free list

关键洞察:池分配器和 monotonic 在分配速度上接近——但池分配器可以逐个回收(deallocate),monotonic 只能全局重置。选择取决于生命周期边界:帧级→monotonic,对象级→池。

# 4.6 分配器传播与 scoped_allocator 的完整推演


# 5. PMR 与多态内存资源

# 5.1 从静态多态到运行时多态

// ❌ 模板方式——类型不同
std::vector<int, PoolAlloc> v1;
std::vector<int, ArenaAlloc> v2;
// v1 和 v2 是不同的类型——不能用统一接口

// ✅ PMR 方式——类型相同
std::pmr::vector<int> v1(&pool);
std::pmr::vector<int> v2(&arena);
// v1 和 v2 完全相同——可以在运行时切换底层 resource
1
2
3
4
5
6
7
8
9

# 5.2 memory_resource 的虚函数接口

class memory_resource {
public:
    virtual ~memory_resource() = default;

    void* allocate(size_t bytes, size_t alignment = alignof(max_align_t)) {
        return do_allocate(bytes, alignment);  // 非虚——可做日志/统计
    }
    void deallocate(void* p, size_t bytes, size_t alignment) {
        return do_deallocate(p, bytes, alignment);
    }
    bool is_equal(const memory_resource& other) const noexcept {
        return do_is_equal(other);
    }
private:
    virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
    virtual void do_deallocate(void* p, size_t bytes, size_t alignment) = 0;
    virtual bool do_is_equal(const memory_resource&) const noexcept = 0;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

虚函数开销:每次分配多一次间接调用(读 vptr + call)。模板分配器编译期直接跳到 operator new——零间接。

# 5.3 pmr::vector 的类型别名

namespace pmr {
    template <typename T>
    using vector = std::vector<T, std::pmr::polymorphic_allocator<T>>;
    // polymorphic_allocator 存 memory_resource*——而非模板策略类型
}
1
2
3
4
5

# 5.4 运行时切换分配器的能力

// 同一个类型——不同 resource
std::pmr::monotonic_buffer_resource frame_pool(1024 * 1024);
std::pmr::unsynchronized_pool_resource global_pool;

std::pmr::vector<int> v;
v = std::pmr::vector<int>(&frame_pool);   // 运行时切到帧池
// ... 帧内操作 ...
v = std::pmr::vector<int>(&global_pool);  // 运行时切到全局池
1
2
3
4
5
6
7
8

# 6. 三种标准 memory_resource

# 6.1 new_delete_resource —— 默认兜底

// 底层仍然是 ::operator new / delete
auto* res = std::pmr::new_delete_resource();
std::pmr::vector<int> v(100, res);
// 等价于 std::vector<int>——但通过虚函数跳转(多一次间接)
1
2
3
4

# 6.2 monotonic_buffer —— 预分配大块

char buf[10 * 1024 * 1024];
std::pmr::monotonic_buffer_resource pool(buf, sizeof(buf));

// 关键特性:不跟踪单个释放——只有全局 reset
pool.release();  // 所有内存直接失效——水位线归零
1
2
3
4
5

使用场景:一帧内大量分配+帧结束统一释放(游戏引擎/请求处理)。

# 6.3 unsynchronized_pool —— 线程本地池

// 每线程独立池——无锁
std::pmr::unsynchronized_pool_resource pool;

// 适合:每个线程有自己的工作数据——不需要跨线程共享池
1
2
3
4

# 6.4 benchmark 性能对比

100 万次 pmr::vector<int> 的 push_back(各 1 个元素):

分配器 时间 说明
new_delete_resource 12 ms 无额外开销
monotonic_buffer 3 ms 无 free、无碎片——最快
unsynchronized_pool 8 ms 线程本地池——无锁
synchronized_pool 18 ms 多线程共享池——有锁

# 7. scoped_allocator 的嵌套传播

# 7.1 嵌套容器分配器难题

// 如果 vector<string> 的 string 内部用默认分配器——即使 vector 用了自定义分配器
std::pmr::vector<std::string> v(&pool);
v.emplace_back("hello");  // string 的内部字符存储在默认堆上——不在 pool 中!
1
2
3

# 7.2 scoped_allocator_adaptor 的递推传播

// scoped_allocator 让分配器递归传播到嵌套容器/元素
std::pmr::vector<std::pmr::string> v(
    std::pmr::polymorphic_allocator<std::pmr::string>(&pool)
);
// 使用 scoped_allocator_adaptor 自动传播:
std::scoped_allocator_adaptor<std::pmr::polymorphic_allocator<std::pmr::string>> sa(&pool);
std::vector<std::pmr::string, decltype(sa)> v2(sa);
v2.emplace_back("hello");  // string 内部也在 pool 中!✅
1
2
3
4
5
6
7
8

# 7.3 什么时候需要 scoped_allocator

场景 是否需要
vector<int>(POD 无内部分配) ❌
vector<string> + 自定义分配器 ✅ 需要 scoped
pmr::vector<pmr::string> + pmr ✅ pmr 自带传播
map<int, vector<int>> + 自定义 ✅ 需要 scoped

# 8. 分配器与异常安全的共生

# 8.1 allocate 失败的标准行为

// 默认:抛 std::bad_alloc
auto* p = alloc.allocate(too_large);

// noexcept 分配器——返回 nullptr
auto* p = noexcept_alloc.allocate(too_large);  // 不抛异常
1
2
3
4
5

# 8.2 分配失败的回滚策略

// vector::push_back 的强异常安全:
// ① allocate 新内存 → 可能抛 bad_alloc
// ② 移动/拷贝旧元素 → 可能抛(非 noexcept)
// ③ deallocate 旧内存
// 如果 ② 失败 → deallocate 新内存 → 旧 vector 不变 ✅
1
2
3
4
5

# 9. 各场景分配器选型

# 9.1 高频交易 / 游戏 / 嵌入式 场景对比

场景 推荐 原因
通用(无特殊需求) std::allocator 最简单——零心智负担
每帧临时数据 monotonic_buffer 帧结束统一释放——零 free
线程本地工作数据 unsynchronized_pool 每线程独立——无锁分配
全程序生命周期 new_delete_resource 依赖 malloc/free——成熟稳定
共享内存 IPC 自定义分配器 在共享内存段分配
调试内存泄漏 自定义追踪分配器 记录每次 alloc/dealloc 的栈

# 9.2 与 jemalloc / tcmalloc 的分工

它们不在同一层:

自定义分配器 / PMR        ← 容器层——控制「如何组织内存」
operator new / delete     ← 接口层——C++ 分配入口
malloc / free             ← 实现层——C 分配器
jemalloc / tcmalloc       ← 替代 malloc——全局替换 LD_PRELOAD
1
2
3
4

可以同时用:自定义分配器减少容器碎片,tcmalloc 加速底层 malloc。


# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① std::allocator 做什么? 第 3 章:allocate→operator new, deallocate→operator delete, rebind→类型转换
② 自定义分配器? 第 4 章:最小实现 4 成员+rebind+operator==;池分配器实战
③ PMR vs 模板? 第 5 章:PMR=运行时多态(同一类型),模板=编译期选定(不同类型)
④ 三种 resource? 第 6 章:new_delete(兜底)、monotonic(帧统一释放)、unsynchronized_pool(线程本地)
⑤ scoped_allocator? 第 7 章:分配器递归传播到嵌套容器的元素
⑥ 分配失败? 第 8 章:默认抛 bad_alloc,容器回滚保持强安全
⑦ vs jemalloc? 第 9 章:不同层——自定义分配器选组织、jemalloc 选底层实现

案例①修复(碎片风暴):场景加载用池分配器——5000 个 Particle 在同一块预分配内存中创建,零碎片。

案例②修复(PMR 释放):monotonic_buffer 只适合「帧内创建+帧结束丢弃」——不适合长期持有对象。

# 10.2 一次 pmr::vector::push_back 的完整分配链路

v.push_back(42);

① std::pmr::vector&lt;int>::push_back  → 检查 capacity
② 需要扩容 → allocator_.allocate(new_capacity)
③ polymorphic_allocator&lt;int>::allocate → resource_->allocate(bytes)
④ memory_resource::allocate → do_allocate(bytes)  ← 虚函数跳转
⑤ monotonic_buffer_resource::do_allocate
    → 从内部 buffer 取下一块 → 更新水位线指针
    → 返回指针——零系统调用
1
2
3
4
5
6
7
8
9

# 10.3 设计哲学回扣

哲学 1:分离关注——内存分配和对象构造是两件独立的事

allocate 只分配裸内存——不调构造函数。deallocate 只释放裸内存——不调析构函数。分配器的职责是「在哪放」,容器的职责是「怎么构造」。 这种分离让同一个分配器可以用于任何可分配的类型——通过 rebind 进行类型转换。

哲学 2:PMR 解决的是「分配器类型膨胀」——一个类型、多种策略

模板分配器的代价是每换一种分配器就产生一个新的容器类型。pmr::vector<int> 只有一个类型——运行时传入不同的 memory_resource*。这是从「编译期多态」到「运行期多态」的经典迁移——牺牲少量性能(一次虚函数调用),换取大量灵活性(一个类型通吃)。

哲学 3:monotonic_buffer 是最极致的「不释放」哲学

monotonic_buffer_resource 从不释放单个分配——只有全局 release()。这个设计告诉你:有时候「不释放」比「精确释放」更快——你不需要维护 free list、不需要处理碎片——一切在同一个连续缓冲区上线性增长。

哲学 4:分配器是可替换的契约——不是魔法

分配器不能解决所有性能问题。它只能控制容器内部元素的放置。如果你的瓶颈是算法复杂度(O(N²) 而非 O(N log N)),换分配器没有用。在优化之前先 profiling——确定瓶颈是分配器还是算法。

# 10.4 速查表合集

分配器选型速查:

需求 推荐
默认(无定制) std::allocator
需要运行时切换策略 std::pmr::polymorphic_allocator + memory_resource
短期临时数据(帧内) monotonic_buffer_resource
线程本地池 unsynchronized_pool_resource
嵌套容器传播 std::scoped_allocator_adaptor

卷五收官——八篇脉络回顾:

32.vector扩容真相    → 连续内存、growth factor、reserve
33.deque分段连续      → map+chunk、两端O(1)、cache 折中
34.list与forward_list → 节点链表、splice、哨兵
35.关联容器红黑树      → 五性质、异构查找、extract
36.哈希容器深度        → 拉链vs开放寻址、SIMD探测、rehash
37.迭代器五大类别      → tag dispatch、traits、C++20 sentinel
38.STL算法设计哲学     → introsort三合一、partition、ranges
39.Allocator分配器机制 → 模板vs PMR、monotonic、scoped
1
2
3
4
5
6
7
8

下一篇:分配器的路径说清了。下一篇进入卷六 40.C++内存模型基石——从多核 CPU 缓存架构到 MESI 协议、Store Buffer 与 Invalidate Queue、为什么 C++ 需要内存模型——并发编程的物理基础。

上次更新: 2026/06/10, 11:13:41
STL算法设计哲学
C++内存模型基石

← STL算法设计哲学 C++内存模型基石→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式