第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 三者如何协同工作
在实际的高频做市系统中,这三个数据结构是配合使用的:
- 内存池负责管理订单对象的生命周期,避免动态内存分配
- 无锁队列负责在多个线程之间传递订单数据,比如行情线程到策略线程
- 订单簿负责维护当前的买卖盘口状态
我画了一张图,展示它们之间的关系:
你看,行情线程从交易所拿到数据后,从内存池里取一个订单对象,塞进无锁队列。策略线程从队列里取出来,更新订单簿,然后做出交易决策。整个过程没有锁,没有动态内存分配,延迟控制在微秒级别。
我的经验:刚开始做这个系统时,我用了三个独立的线程池来管理不同的对象。后来发现,其实一个统一的内存池就够了,只要提前算好最大订单数。这样还能减少内存碎片。
好了,这一章的内容就到这里。记住,订单簿、内存池、无锁队列这三个东西,是高频交易系统的基石。把它们搞透了,后面的策略实现就水到渠成。