文档目录

好的,我们从第一个盲区开始补齐:内存分配器(Memory Allocator)。

这是无锁编程中最容易被忽视的"隐藏杀手"。你精心设计了一个无锁队列,所有 CAS 操作都完美,结果发现性能还是上不去——很可能是 new 和 delete 在背后拖了后腿。


1. 问题:malloc 是个"全局锁"

当我们调用 new / delete(底层是 malloc / free)时,背后是一个全局堆管理器在干活。

[线程A] new 对象 → 进入 malloc()
                    │
                    ▼
              [全局堆锁] ← 线程A 拿到锁,开始分配内存
                    │
[线程B] new 对象 → 进入 malloc() → 等待全局堆锁 → 阻塞!
[线程C] new 对象 → 进入 malloc() → 等待全局堆锁 → 阻塞!

硬件视角:

  • 全局堆锁本质上是一个 Mutex(或自旋锁),多个线程争抢时会产生缓存行乒乓(MESI RFO)。
  • 每次 malloc/free 都可能触发系统调用(brk / mmap),陷入内核态。
  • 频繁分配/释放时,锁开销可能超过业务逻辑本身。

结论:无锁数据结构本身很快,但 malloc 的全局锁会让你的多线程程序退化成串行执行。


2. 解决方案:Per-CPU / Per-Thread 内存池

核心思想:让每个线程/核心拥有自己独立的内存池,分配时无需加锁。

方案 A:jemalloc(Firefox / Redis / Rust 默认)

jemalloc 的核心设计:

+----------------------------------------------------------+
|                      jemalloc 架构                         |
+----------------------------------------------------------+
|  每个线程有自己的 tcache(Thread Cache)                   |
|  +--------------------------------------------------+    |
|  | tcache:小型对象缓存(无锁,极快)                 |    |
|  |   - 16B 对象列表                                   |    |
|  |   - 32B 对象列表                                   |    |
|  |   - 64B 对象列表                                   |    |
|  |   - ...                                           |    |
|  +--------------------------------------------------+    |
|                      ↓ (tcache 满了或对象太大)            |
|  每个 CPU 核心有自己的 arena(竞技场)                    |
|  +--------------------------------------------------+    |
|  | arena:中型对象池(多线程共享,但每个核心一个)    |    |
|  +--------------------------------------------------+    |
|                      ↓ (arena 也满了)                    |
|  全局堆(最终后备,需要加锁)                             |
+----------------------------------------------------------+

分配流程:

[线程A 申请 64B 对象]
        │
        ▼
[检查 tcache] → 有 → 直接返回(~5ns,无锁)
        │
        ▼ 无
[检查对应核心的 arena] → 有 → 返回(~20ns,可能需原子操作)
        │
        ▼ 无
[全局堆] → 加锁分配(~100ns,有竞争)

性能对比:

分配器 单线程分配 (ns) 多线程分配 (ns) 锁竞争
glibc malloc ~50 ~300+(随线程数飙升) 严重
jemalloc ~15 ~30(稳定) 极小
tcmalloc ~10 ~25(稳定) 极小

方案 B:tcmalloc(Google,TensorFlow / Go 早期使用)

tcmalloc 的设计与 jemalloc 类似,但更强调线程本地缓存(Thread-Local Cache):

[线程A] → [Thread-Local Cache] → 无锁分配,命中率 ~95%
              │
              ↓ (未命中)
         [中央堆(Central Heap)] → 加锁分配,但频率低

特点:

  • 小对象(< 256KB)优先从 Thread-Local Cache 分配。
  • 大对象直接用 mmap 分配,绕过分级缓存。

3. 如果你需要定制:手写固定大小的对象池(Object Pool)

在极致性能场景(如网络收包的 Buffer 池),你可以自己实现一个无锁对象池。

template<typename T, size_t PoolSize>
class ObjectPool {
    // 使用 SPSC 队列思想:生产者 = 归还对象,消费者 = 获取对象
    // 但这里我们用 Lock-Free Stack 实现
    struct Node {
        T data;
        std::atomic<Node*> next;
    };
    
    alignas(64) std::atomic<Node*> free_list{nullptr};
    alignas(64) std::atomic<Node*> allocated_list{nullptr};
    
    // 预分配池子(一次性分配,避免运行时 malloc)
    Node pool[PoolSize];

public:
    ObjectPool() {
        // 将所有节点串成空闲链表
        for (size_t i = 0; i < PoolSize - 1; ++i) {
            pool[i].next = &pool[i + 1];
        }
        pool[PoolSize - 1].next = nullptr;
        free_list.store(&pool[0], std::memory_order_relaxed);
    }
    
    // 从池子中取出一个对象(无锁)
    T* allocate() {
        Node* old_head = free_list.load(std::memory_order_acquire);
        Node* new_head;
        do {
            if (!old_head) return nullptr; // 池子已空
            new_head = old_head->next;
        } while (!free_list.compare_exchange_weak(old_head, new_head,
                  std::memory_order_release, std::memory_order_relaxed));
        return &old_head->data;
    }
    
    // 归还对象到池子(无锁)
    void deallocate(T* ptr) {
        Node* node = reinterpret_cast<Node*>(ptr);
        Node* old_head = free_list.load(std::memory_order_acquire);
        do {
            node->next = old_head;
        } while (!free_list.compare_exchange_weak(old_head, node,
                  std::memory_order_release, std::memory_order_relaxed));
    }
};

硬件收益:

  • 所有操作在用户态完成,无系统调用。
  • 使用 CAS 无锁入栈/出栈,无全局锁。
  • Pool 在构造时一次性 malloc 大量内存,后续零分配。

4. 内存分配器与无锁队列的配合

在你的 ScoreMaster 系统中,每一帧可能都需要分配内存(如检测框列表、NPU 输入输出 Buffer)。如果每一帧都 new / delete,全局堆锁会成为瓶颈。

推荐架构:

[Decoder Thread] → 从 BufferPool 申请 Frame 对象
        │
        ▼
[Pipeline Thread] → 处理完,归还到 BufferPool
        │
        ▼
[NPU Workers] → 从 BufferPool 申请 Result 对象
        │
        ▼
[Qt Main Thread] → 渲染完,归还到 BufferPool

全链路无锁 + 零 malloc。


5. 笔记存档:内存分配器核心概念速查

概念 解释
全局堆锁 glibc malloc 的默认行为,多线程竞争时严重拖慢性能。
tcache(Thread Cache) jemalloc / tcmalloc 的核心:每个线程独立的小对象缓存,无锁分配。
arena jemalloc 中每个 CPU 核心的缓存区,减少跨核心竞争。
对象池(Object Pool) 预先分配固定数量对象,用无锁栈(CAS)管理,完全绕开 malloc。
适用场景 频繁分配/释放的场景(网络包处理、游戏对象、帧数据)。
不适用场景 对象大小差异巨大、生命周期极长、总内存需求不可预估。

本节关键词:内存分配器、全局堆锁、jemalloc、tcmalloc、tcache、arena、对象池、无锁分配。 与前期知识的关联:SPSC/MPSC 队列 + 对象池 = 零 malloc、零锁、零系统调用的全链路数据管道。