数据结构优化:选对容器,性能翻倍

行情数据和订单簿,是量化交易系统的两个心脏。一个负责感知市场,一个负责记录博弈。这两个数据结构选不对,后面再怎么优化都是白搭。

我见过太多团队,上来就用 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 微秒以内。

数据结构选择的黄金法则

说了这么多,我总结几条经验法则:

  1. 读多写少用数组:行情数据、历史回测,数组永远是首选
  2. 写多读多用跳表:订单簿、事件队列,跳表比红黑树更灵活
  3. 需要快速查找用哈希表:但要注意内存占用和哈希冲突
  4. 需要顺序访问用链表:但别用单链表,双向链表更实用
  5. 组合结构才是王道:没有万能的数据结构,组合使用才能发挥最大效能

记住:数据结构选对了,性能优化就成功了一半。选错了,后面再怎么优化都是事倍功半。

嗯,今天就聊到这儿。数据结构这块儿,说白了就是「因地制宜」。没有最好的结构,只有最合适的结构。你在项目中遇到什么奇葩的数据结构问题,欢迎来交流。

量化交易系统数据结构选择框架 行情数据与订单簿 行情数据(时间序列) 订单簿(多维数据) 数组 / 环形数组 Tick 数据、K 线序列 向量 / 列表 历史回测数据 跳表 价格层管理 哈希表 + 双向链表 订单队列管理 红黑树 备选方案 选择依据:读写比例、数据量、并发需求、内存限制 组合使用,因地制宜
专注资料整理