std::map和std::unordered_map在订单薄中是内存黑洞,因其默认实现包含红黑树指针、allocator元数据及对齐填充,使每个订单节点额外占用32–48字节;实测10万档价格订单薄用std::map超12 MB,远高于紧凑结构。

为什么 std::map 和 std::unordered_map 在订单薄里是内存黑洞
金融撮合对延迟和内存都极度敏感,而标准容器默认分配策略会让每个订单节点额外吃掉 32–48 字节(红黑树指针 + allocator 元数据 + 对齐填充)。实测一个含 10 万档价格的订单薄,用 std::map<double orderlist></double> 可能占用 >12 MB;换成紧凑结构后压到
关键不是“能不能用”,而是“每纳秒和每字节是否值得”。订单薄中价格档位实际稀疏(尤其在远离市价处),但标准 map 仍为每个键维护完整节点;更糟的是,std::unordered_map 的桶数组+链表结构在小负载时空间放大比常达 3x 以上,且哈希冲突导致缓存不友好。
- 避免动态分配单个订单节点:改用 arena 分配器或预分配 slab 块
- 价格档位不用浮点键直接索引:转为
int64_t price_ticks(如 1e-5 USD = 1 tick),消除浮点哈希/比较开销 - 放弃“键值分离”思维:把价格档作为连续数组下标,而非查找目标
用 std::vector + 稀疏索引实现 O(1) 档位访问
主流交易所(如 Binance、Coinbase)的限价订单薄实际活跃档位通常 vector
典型做法是维护两个结构:std::vector<orderbucket></orderbucket> 存所有非空档位,外加一个 std::array<int16_t max_price_range></int16_t>(比如 65536)做快速跳转表——只存有效档位在 vector 中的下标,无效档位填 -1。这样 getBucketAtPrice(price) 就是纯数组访问,无分支、无 cache miss。
立即学习“C++免费学习笔记(深入)”;
-
MAX_PRICE_RANGE要按 instrument 预设(如 BTC-USDT 用 1e-1 tick,范围 0~10^7 → 改用两级索引或 hash 编码) - vector 用
reserve()预分配,避免扩容拷贝;OrderBucket 内部用std::array<order></order>存订单指针,避免链表遍历 - 删除订单时仅置空指针、标记 slot 为 free;归还内存统一在 batch flush 阶段做
Order 结构体怎么做到
一个订单真正需要持久化的核心字段其实就四个:order_id(uint64)、price_ticks(int64)、remaining_qty(int32)、side(1 bit)。其他如时间戳、用户 ID、订单类型可外部关联或压缩存储。
重点在取消操作:不能靠遍历找订单。解决方案是在 OrderBucket 内维护 std::array<uint32_t max_orders_per_bucket></uint32_t>,存每个订单在全局订单池(arena)中的偏移;同时在 Order 结构体内嵌 bucket_idx 和 slot_in_bucket(各占 16 bit),形成双向定位。取消时根据 order_id 直接算出 bucket 和 slot,O(1) 完成标记。
- 把
order_id设计为自增 uint64,高位含 session ID + 时间戳,低位是序列号,便于分片和去重 - 不要在
Order里存指针——指针在不同进程/共享内存中会失效;一律用相对偏移或索引 - 剩余数量用 int32 即可(多数资产最小变动单位足够大),省下 4 字节;side 用 bit field 压进同一 word
共享内存场景下如何避免 std::allocator 导致的段错误
多进程撮合引擎(如 C++ 进程 + Python 策略监控)常通过 POSIX 共享内存(shm_open)交换订单薄。此时任何使用默认 std::allocator 的容器都会崩——它分配的地址只在本进程有效。
必须全部替换为基于 mmap 地址的自定义分配器。最简方案:用 boost::interprocess::allocator,但它依赖 Boost。更轻量的做法是手写一个只调用 mmap(MAP_FIXED) 的固定地址分配器,并确保所有结构体满足 trivially copyable 且不含虚函数。
- 订单薄根结构体(含 vector header、索引表、arena head)必须放在共享内存起始位置,且 size 固定(用
static_assert(sizeof(Book) == 4096)卡死) - 禁止在共享内存中使用
std::string、std::shared_ptr、任何 STL 容器——它们内部指针无法跨进程解析 - 调试时用
gdb attach查看共享内存地址是否一致;若看到0x7fxxxxxx和0x2axxxxxx混用,说明某处偷偷用了堆分配
极致优化从来不是堆上少几个字节,而是让每一级缓存行都装满有效数据、让每一次取消都落在 L1d 里、让共享内存里没有一个字节需要 runtime 解析。这些细节在回测里看不出差别,但在真实盘口跳动时,就是吞吐差三倍、延迟抖动翻五倍的分水岭。


















