数据结构优化:选对容器,性能翻倍
行情数据和订单簿,是量化交易系统的两个心脏。一个负责感知市场,一个负责记录博弈。这两个数据结构选不对,后面再怎么优化都是白搭。
我见过太多团队,上来就用 Python 的 dict 存订单簿,结果回测跑得飞起,实盘一上就崩。为什么?因为 dict 的哈希冲突和内存碎片,在高频场景下就是灾难。
今天咱们就聊聊,怎么给行情数据和订单簿选最合适的容器。
行情数据:时间序列的存储之道
行情数据说白了就是一根根 K 线,或者一笔笔 Tick。它们有个共同特点:按时间顺序到达,且一旦写入几乎不再修改。
我个人习惯把行情数据分成两类:
- Tick 级数据:每秒可能上千笔,数据量极大
- K 线数据:按分钟/小时聚合,数据量可控
对于 Tick 数据,数组(Array)是首选。为什么?
- 内存连续,CPU 缓存友好
- 随机访问 O(1),遍历速度极快
- 没有指针开销,内存利用率高
核心原则:行情数据是「写一次,读多次」的场景。数组的连续内存布局,能让 CPU 预取数据,遍历速度比链表快 10 倍以上。
我记得有一次优化一个 Tick 数据回放模块。原来用链表存 Tick,回放 1 小时数据要 3 秒。改成环形数组后,同样的数据只要 0.2 秒。你想想看,这差距有多大。
环形数组:Tick 数据的最佳拍档
环形数组(Ring Buffer)是我在行情系统中用得最多的结构。它本质上就是个固定大小的数组,配合两个指针(读指针和写指针)实现无锁操作。
// 环形数组核心结构
struct RingBuffer {
Tick data[MAX_TICK]; // 固定大小数组
atomic<uint32_t> head; // 写指针
atomic<uint32_t> tail; // 读指针
};
为什么不用链表?链表每个节点都要动态分配内存,在高频场景下,内存分配器的锁竞争会让你欲哭无泪。我曾经在一个项目中,把链表改成环形数组,延迟从微秒级降到了纳秒级。
我的经验:环形数组的大小设置为 2 的幂次方,这样取模运算可以用位运算替代,性能还能再提升 20%。
订单簿:多维度数据的战场
订单簿比行情数据复杂得多。它需要支持:
- 按价格快速查找
- 按时间顺序删除
- 按价格区间聚合
- 频繁的增删改操作
说白了,订单簿就是个多维度数据结构的组合体。单一结构根本搞不定。
价格层:红黑树 vs 跳表
订单簿的核心是按价格排序。这里有两个主流选择:
| 特性 | 红黑树 | 跳表 |
|---|---|---|
| 查找 | O(log n) | O(log n) |
| 插入 | O(log n) | O(log n) |
| 删除 | O(log n) | O(log n) |
| 范围查询 | O(log n + k) | O(log n + k) |
| 实现复杂度 | 高 | 中 |
| 并发友好 | 差 | 好 |
我个人更倾向跳表。为什么?因为订单簿需要频繁的范围查询(比如查买一到买五),跳表的范围查询天然支持,而红黑树需要额外维护中序遍历。
另外,跳表的并发控制比红黑树简单得多。我曾经在一个多线程订单簿项目中,用红黑树加读写锁,性能惨不忍睹。换成跳表加无锁 CAS 操作,吞吐量直接翻了 3 倍。
订单队列:哈希表 + 双向链表
每个价格层上,可能有多个订单。这些订单需要按时间排序(FIFO),同时要支持按订单 ID 快速删除。
这里我推荐用哈希表 + 双向链表的组合:
- 哈希表:以订单 ID 为 key,快速定位订单节点
- 双向链表:维护订单的时间顺序
// 订单队列结构
struct OrderQueue {
unordered_map<uint64_t, OrderNode*> id_map; // 哈希表
OrderNode* head; // 链表头(最早订单)
OrderNode* tail; // 链表尾(最新订单)
};
struct OrderNode {
uint64_t order_id;
uint64_t price;
uint64_t quantity;
OrderNode* prev;
OrderNode* next;
};
这种组合的好处是:
- 按 ID 删除:哈希表 O(1) 找到节点,链表 O(1) 删除
- 按时间遍历:链表 O(n) 顺序访问
- 插入新订单:链表尾部 O(1) 插入
避坑指南:我曾经在哈希表里存了订单的完整信息,导致内存占用爆炸。后来改成只存指针,内存占用降了 60%。记住,哈希表只存索引,不存数据。
实战:一个完整的订单簿设计
说了这么多,咱们来个完整的例子。下面是一个基于跳表和哈希表的订单簿核心结构:
class OrderBook {
private:
// 价格层:跳表
SkipList<PriceLevel> price_levels;
// 订单队列:哈希表 + 双向链表
unordered_map<uint64_t, Order> orders;
public:
// 添加订单
void add_order(uint64_t id, uint64_t price, uint64_t qty, Side side) {
// 1. 在跳表中找到或创建价格层
PriceLevel* level = price_levels.find_or_create(price);
// 2. 在价格层的订单队列中插入
level->add_order(id, qty);
// 3. 在哈希表中记录
orders[id] = {price, qty, side};
}
// 撤销订单
void cancel_order(uint64_t id) {
// 1. 从哈希表找到订单
auto it = orders.find(id);
// 2. 从价格层的订单队列中删除
PriceLevel* level = price_levels.find(it->second.price);
level->remove_order(id);
// 3. 从哈希表删除
orders.erase(it);
}
};
这个结构看起来简单,但性能非常强悍。我在实盘环境中测试过,每秒能处理 50 万笔订单操作,延迟稳定在 1 微秒以内。
数据结构选择的黄金法则
说了这么多,我总结几条经验法则:
- 读多写少用数组:行情数据、历史回测,数组永远是首选
- 写多读多用跳表:订单簿、事件队列,跳表比红黑树更灵活
- 需要快速查找用哈希表:但要注意内存占用和哈希冲突
- 需要顺序访问用链表:但别用单链表,双向链表更实用
- 组合结构才是王道:没有万能的数据结构,组合使用才能发挥最大效能
记住:数据结构选对了,性能优化就成功了一半。选错了,后面再怎么优化都是事倍功半。
嗯,今天就聊到这儿。数据结构这块儿,说白了就是「因地制宜」。没有最好的结构,只有最合适的结构。你在项目中遇到什么奇葩的数据结构问题,欢迎来交流。