4. C++低延迟编程基础:内存管理

做高频交易的朋友都知道,延迟就是金钱。你比别人慢一微秒,可能单子就成交在别人后面了。今天咱们聊聊C++低延迟编程里最基础、也最关键的一块——内存管理。

我个人习惯把内存管理比作「战场后勤」。你算法再牛,策略再强,如果内存这块拖后腿,一切都白搭。咱们从三个角度切入:栈 vs 堆、对象池、缓存友好设计,最后再聊无锁数据结构。

4.1 栈 vs 堆:谁更快?

先问个问题:你写代码时,变量是分配在栈上还是堆上?

栈分配,说白了就是编译器帮你打理。函数调用时压栈,返回时弹栈。整个过程就是移动一下栈指针,几纳秒的事。我刚开始做量化时,有个同事把所有订单对象都new在堆上,结果回测跑得巨慢。后来改成栈分配,速度直接翻倍。

堆分配呢?需要调用malloc或new,操作系统得找一块空闲内存,还可能触发缺页中断。你想想看,这中间差了多少个数量级?

核心结论:高频场景下,能用栈就别用堆。栈分配延迟通常在10纳秒以内,堆分配可能到几百纳秒甚至微秒级。

但现实是,有些对象生命周期长,或者大小不确定,必须用堆。这时候怎么办?

4.2 对象池:堆分配的救星

我曾经在一个做市商项目里遇到个坑:每秒要处理几十万笔订单,每个订单都new/delete,结果GC(垃圾回收)把性能拖垮了。嗯,这里要注意——C++虽然没有GC,但频繁的堆分配释放会导致内存碎片,分配器越来越慢。

解决方案就是对象池。说白了,提前分配好一大块内存,用的时候从池子里拿,用完放回去。没有malloc,没有free,只有指针的移动。

// 一个简单的对象池实现
template<typename T>
class ObjectPool {
private:
    char* buffer;          // 预分配的内存块
    T** free_list;         // 空闲对象指针数组
    size_t pool_size;
    size_t free_count;
    
public:
    ObjectPool(size_t size) : pool_size(size), free_count(size) {
        buffer = new char[size * sizeof(T)];
        free_list = new T*[size];
        for (size_t i = 0; i < size; ++i) {
            free_list[i] = reinterpret_cast<T*>(buffer + i * sizeof(T));
        }
    }
    
    T* acquire() {
        if (free_count == 0) return nullptr;
        return free_list[--free_count];
    }
    
    void release(T* obj) {
        free_list[free_count++] = obj;
    }
    
    ~ObjectPool() {
        delete[] buffer;
        delete[] free_list;
    }
};

这个实现虽然简单,但已经能解决大部分问题了。我在实际项目中,对象池的分配速度基本和栈分配持平,都在10纳秒以内。

个人经验:对象池的大小要提前估算好。太小了会频繁扩容,太大了浪费内存。我一般按峰值流量的1.5倍来预分配。

4.3 缓存友好设计:Cache Line对齐与预取

你想想看,CPU从内存读数据有多慢?大概100纳秒。但从L1缓存读呢?不到1纳秒。差了100倍!所以,让数据待在缓存里,是低延迟编程的核心。

Cache Line是CPU缓存的最小单位,通常是64字节。如果两个线程修改同一个Cache Line里的不同变量,就会发生「伪共享」——明明没共享数据,却因为缓存一致性协议互相拖累。

我曾经调试过一个诡异的问题:两个线程各自处理自己的订单队列,性能就是上不去。后来用perf一看,L1缓存miss率高达30%。原因就是两个队列的头部指针恰好在一个Cache Line里。

解决方案很简单:Cache Line对齐。

// 使用alignas强制对齐到64字节
struct alignas(64) OrderQueue {
    int head;
    char padding[60];  // 填充到64字节
    int tail;
};

另一个技巧是预取。如果你知道接下来要访问某个内存地址,可以提前告诉CPU把它加载到缓存里。

// 预取下一个订单
__builtin_prefetch(&orders[i + 8], 0, 3);
process(orders[i]);

这里的参数含义:0表示读预取,3表示高优先级。我习惯在遍历大数组时,提前8-16个元素做预取,效果很明显。

避坑指南:预取不是万能的。预取太早,数据可能被踢出缓存;预取太晚,来不及加载。我曾经在循环里每步都预取,结果反而变慢了。后来改成每4步预取一次,效果最好。

4.4 无锁数据结构:Lock-Free Queue

锁是低延迟的天敌。一个线程拿锁,其他线程就得等着。在高频交易里,这种等待可能就是丢单。

无锁数据结构,说白了就是利用CPU的原子指令(CAS、FAA等)来实现线程安全,不用操作系统级别的锁。我最早接触无锁队列是在一个行情分发系统里,那时候用互斥锁,延迟在微秒级。换成无锁队列后,直接降到几百纳秒。

下面是一个经典的无锁队列实现——Michael-Scott队列的简化版:

template<typename T>
class LockFreeQueue {
private:
    struct Node {
        T data;
        std::atomic<Node*> next;
        Node(const T& val) : data(val), next(nullptr) {}
    };
    
    std::atomic<Node*> head;
    std::atomic<Node*> tail;
    
public:
    LockFreeQueue() {
        Node* dummy = new Node(T());
        head.store(dummy);
        tail.store(dummy);
    }
    
    void push(const T& val) {
        Node* new_node = new Node(val);
        Node* old_tail;
        while (true) {
            old_tail = tail.load();
            Node* next = old_tail->next.load();
            if (old_tail == tail.load()) {
                if (next == nullptr) {
                    if (old_tail->next.compare_exchange_weak(next, new_node)) {
                        break;
                    }
                } else {
                    tail.compare_exchange_weak(old_tail, next);
                }
            }
        }
        tail.compare_exchange_weak(old_tail, new_node);
    }
    
    bool pop(T& result) {
        Node* old_head;
        while (true) {
            old_head = head.load();
            Node* old_tail = tail.load();
            Node* next = old_head->next.load();
            if (old_head == head.load()) {
                if (old_head == old_tail) {
                    if (next == nullptr) return false;
                    tail.compare_exchange_weak(old_tail, next);
                } else {
                    result = next->data;
                    if (head.compare_exchange_weak(old_head, next)) {
                        break;
                    }
                }
            }
        }
        delete old_head;
        return true;
    }
};

这个实现的核心是CAS(Compare-And-Swap)操作。注意看push和pop里的while循环——这就是无锁编程的典型模式:不断尝试,直到成功。

关键点:无锁队列的ABA问题。简单说,就是指针被重用导致CAS误判。解决方案是使用带标签的指针(如std::atomic<uint64_t>,高32位存指针,低32位存版本号)。

我在实际项目中,无锁队列的延迟通常在50-100纳秒之间,远低于互斥锁的微秒级。但要注意,无锁编程调试起来非常痛苦。我曾经花了一周时间,才找到一个ABA问题导致的偶发崩溃。

4.5 知识体系总览

下面这张图,是我对本章知识结构的总结:

C++低延迟编程基础:内存管理知识体系 内存分配策略 栈分配(~10ns) 堆分配(~100ns+) 对象池(~10ns) 缓存友好设计 Cache Line对齐 伪共享避免 数据预取 无锁数据结构 CAS原子操作 Lock-Free Queue ABA问题处理 核心原则 1. 能用栈不用堆,减少分配开销 2. 对象池解决高频堆分配问题 3. 缓存对齐 + 预取,让数据靠近CPU 4. 无锁数据结构,避免锁竞争

这张图把本章的核心内容串起来了。从左到右,从内存分配到缓存优化,再到无锁数据结构,每一步都是为了降低延迟。

最后说一句:这些技术不是孤立的。在实际项目中,我经常把对象池和无锁队列结合起来用——对象池管理内存,无锁队列管理任务。两者配合,延迟能压到极致。

嗯,今天就聊到这儿。这些基础打牢了,后面讲更高级的算法时,你才能游刃有余。


专注资料整理