第1章:C++核心数据结构——订单簿、内存池与无锁队列

做高频交易的人都知道,行情数据来了,你必须在微秒级别内做出反应。订单簿(OrderBook)就是那个最核心的数据结构。说白了,它就是个实时更新的买卖盘口。我刚开始做量化那会儿,用的还是Python的dict来存订单,结果回测跑得挺欢,一上实盘就崩——延迟太高了。

今天咱们就来聊聊,怎么用C++把订单簿、内存池和无锁队列这三个东西做到极致。嗯,这些都是我踩过坑之后总结出来的经验。

1.1 订单簿(OrderBook)设计

订单簿的核心逻辑其实不复杂:维护一个买盘(Bid)和一个卖盘(Ask),按价格排序。买盘从高到低,卖盘从低到高。每次来一个新订单,要么成交,要么挂单。

我建议用std::map来存价格档位,因为它是红黑树实现,插入和删除都是O(log n)。但注意,这里有个坑——std::map的迭代器在插入时不会失效,但删除时会。我在项目中遇到过,一个不小心就用了失效的迭代器,查了半天bug。

// 订单簿核心结构
struct Order {
    uint64_t order_id;
    double price;
    uint32_t quantity;
    char side; // 'B' 买, 'S' 卖
    uint64_t timestamp;
};

class OrderBook {
public:
    // 添加订单
    void add_order(const Order& order) {
        if (order.side == 'B') {
            bids_[order.price] += order.quantity;
        } else {
            asks_[order.price] += order.quantity;
        }
    }
    
    // 删除订单
    void remove_order(uint64_t order_id) {
        // 这里需要维护一个 order_id 到 price 的映射
        // 否则你不知道该删哪个价格档位
    }
    
private:
    std::map<double, uint32_t, std::greater<double>> bids_; // 买盘,降序
    std::map<double, uint32_t, std::less<double>> asks_;    // 卖盘,升序
};

关键点:订单簿的更新速度决定了你的策略能否抢到单。我见过有人用std::unordered_map,结果价格档位多了之后,哈希冲突严重,性能反而更差。

1.2 内存池(MemoryPool)实现

做高频交易,最忌讳的就是频繁new/delete。每次分配内存都要走系统调用,那延迟直接上天。内存池就是提前申请一大块内存,然后自己管理分配和回收。

我个人的习惯是用固定大小的内存池。为什么?因为订单结构体的大小是固定的,没必要搞复杂的内存管理。你想想看,如果每个订单大小不一样,那内存碎片问题就来了。

template<typename T>
class MemoryPool {
public:
    MemoryPool(size_t size) : pool_size_(size) {
        pool_ = static_cast<T*>(std::malloc(size * sizeof(T)));
        // 初始化空闲链表
        for (size_t i = 0; i < size - 1; ++i) {
            *(reinterpret_cast<T**>(pool_ + i)) = pool_ + i + 1;
        }
        *(reinterpret_cast<T**>(pool_ + size - 1)) = nullptr;
        free_head_ = pool_;
    }
    
    T* allocate() {
        if (!free_head_) return nullptr;
        T* obj = free_head_;
        free_head_ = *(reinterpret_cast<T**>(free_head_));
        return obj;
    }
    
    void deallocate(T* obj) {
        *(reinterpret_cast<T**>(obj)) = free_head_;
        free_head_ = obj;
    }
    
private:
    T* pool_;
    T* free_head_;
    size_t pool_size_;
};

避坑指南:我曾经在内存池里犯过一个低级错误——忘记对齐。如果你的订单结构体里有double类型,而内存地址没有8字节对齐,那性能会下降30%以上。记得用std::aligned_storage或者alignas

1.3 无锁队列(LockFreeQueue)原理

多线程环境下,用锁是最简单的方案。但锁会带来上下文切换,在高频交易里这是致命的。无锁队列就是通过CAS(Compare-And-Swap)操作来实现线程安全,没有锁的开销。

我常用的无锁队列是Michael-Scott队列,基于链表实现。它的核心思想是:用原子操作来管理头尾指针。

template<typename T>
class LockFreeQueue {
public:
    LockFreeQueue() {
        Node* dummy = new Node();
        head_.store(dummy, std::memory_order_relaxed);
        tail_.store(dummy, std::memory_order_relaxed);
    }
    
    void push(const T& value) {
        Node* new_node = new Node(value);
        Node* old_tail;
        while (true) {
            old_tail = tail_.load(std::memory_order_acquire);
            Node* next = old_tail->next.load(std::memory_order_acquire);
            if (old_tail == tail_.load(std::memory_order_acquire)) {
                if (next == nullptr) {
                    // CAS 尝试更新 next 指针
                    if (old_tail->next.compare_exchange_weak(
                        next, new_node, 
                        std::memory_order_release, 
                        std::memory_order_relaxed)) {
                        break;
                    }
                } else {
                    // 帮助推进 tail
                    tail_.compare_exchange_weak(
                        old_tail, next,
                        std::memory_order_release,
                        std::memory_order_relaxed);
                }
            }
        }
        // 更新 tail
        tail_.compare_exchange_weak(
            old_tail, new_node,
            std::memory_order_release,
            std::memory_order_relaxed);
    }
    
private:
    struct Node {
        T data;
        std::atomic<Node*> next;
        Node() : next(nullptr) {}
        Node(const T& val) : data(val), next(nullptr) {}
    };
    
    std::atomic<Node*> head_;
    std::atomic<Node*> tail_;
};

注意:无锁队列虽然快,但ABA问题是个大坑。简单说就是,一个节点被释放后又重新分配,地址相同但内容变了。我建议用带引用计数的方案,或者用Hazard Pointer来避免这个问题。

1.4 三者如何协同工作

在实际的高频做市系统中,这三个数据结构是配合使用的:

  • 内存池负责管理订单对象的生命周期,避免动态内存分配
  • 无锁队列负责在多个线程之间传递订单数据,比如行情线程到策略线程
  • 订单簿负责维护当前的买卖盘口状态

我画了一张图,展示它们之间的关系:

高频做市系统核心数据结构协同图 行情线程 接收交易所数据 内存池 (订单对象) 无锁队列 策略线程 处理订单并决策 订单簿 (OrderBook) 内存池 (订单对象) 数据源 内存池 无锁队列 策略线程 订单簿

你看,行情线程从交易所拿到数据后,从内存池里取一个订单对象,塞进无锁队列。策略线程从队列里取出来,更新订单簿,然后做出交易决策。整个过程没有锁,没有动态内存分配,延迟控制在微秒级别。

我的经验:刚开始做这个系统时,我用了三个独立的线程池来管理不同的对象。后来发现,其实一个统一的内存池就够了,只要提前算好最大订单数。这样还能减少内存碎片。

好了,这一章的内容就到这里。记住,订单簿、内存池、无锁队列这三个东西,是高频交易系统的基石。把它们搞透了,后面的策略实现就水到渠成。

专注资料整理