跳到主内容
@wquguru
精选85愚夫一得研究与分析

从做早餐悟出的低延迟系统设计之道

做早餐 与 低延迟 设计之道

原文
发到 X
推荐理由

本文是低延迟交易系统设计的硬核技术分享,从生活案例切入,深入浅出地讲解了决策点前移、编译期计算、增量计算等核心优化思想,并附有代码示例。适合量化交易开发者、系统架构师研究学习,可据此优化自己的交易系统。

我从做早餐中悟出的低延迟系统设计之道。

每天清晨给孩子做一顿热气腾腾的早餐,是一件幸福却伴随着早起痛苦的事。

很长一段时间里,我就是这么"痛并快乐着"地过来的。直到有一天,我在反思这个流程时,突然意识到一个被"线性思维"遮蔽的事实:

为什么早餐必须在早上从头做?

一旦打破这个思维定式,优化路径就清晰了——把所有准备工作的"时间点"前移。前一晚把食材洗净切好,靠电饭煲或空气炸锅的预约功能定时启动。第二天孩子起床洗漱完毕,热乎乎的早餐刚好出锅。

这个不起眼的改动,让我每天多了 30 分钟睡眠。

它的核心,是把一个原本线性的过程一刀切成两段:

  • • 固定部分提前做:食材的清洗、切配——这些事的答案,前一晚就已经确定了。
  • • 灵活部分按需做:加热的火候、每个人不同的口味——这些事必须等客户提需求时才能定。
  • 这种"两段式"思维,在现代工业里其实早就运用到极致:汽车业先造标准化底盘和发动机,最后按订单组装内饰;连锁餐饮中央厨房完成九成预制,门店只做最后一成加热装盘。它们的底层逻辑是同一句:

剥离确定性工作,压降最终交付的"响应延迟"。

同样的思想,也可以落地到低延迟系统设计的全流程中去。

加速订单流水线

把一笔订单的处理也看成一条极速流水线,"做早餐"的智慧立刻适用。低延迟交易的口诀,一句话讲透:

尽可能把计算与访存前置,以空间换时间。

那些在盘中被反复使用的"数据状态"和"确定性逻辑",绝不能等行情到来的瞬间再去现找、现算。它们必须长期驻留,时刻处于子弹上膛的待发状态。

其实,它们背后共享一个更锋利的思维工具 —— 决策点 。

每一件要做的事,都有一个"最早可以被完成的时刻"。优化,就是不断追问:这件事,最早能在什么时候ready?

然后,把它尽量往前挪。

你会发现,它们之间的差别仅仅在于——决策点被前移到了哪一刻。能挪到编译期的,绝不留到盘前;能挪到盘前的,绝不留到盘中;只有那些"客户开口才能定"的事,才留在最后。

带着这把尺子,我们看代码。

决策点前移

为了让你一眼看懂每段代码的价值,我都会先给出笨办法(决策点靠后,慢),再给出聪明办法(决策点前移,快),并标注它们各自把决策点挪到了哪里。

贯穿全文的是一个统一的领域模型:一笔订单 Order,要找到它对应的持仓 Position,校验客户权限、算保证金、做风控。

图模型加速订单处理

笨办法(收到订单后的处理,如决策点在盘中,每次都找):

代码 · 1
// 每笔订单都要现查三张表auto* client   = clientTable_.find(order.clientId);      // 查一次auto* contract = contractTable_.find(order.contractId);  // 再查一次auto* member   = memberTable_.find(order.memberId);      // 再查一次// 三次 hash、三次内存跳转,全压在热路径上

聪明办法(决策点前移到首次访问):

以"会员 + 客户 + 合约"三元组定位唯一的持仓节点,把上面三张表的数据挂载到这个持仓记录的节点上。首次访问时建好关联,之后所有订单复用。

代码 · 1
// 持仓节点:struct alignas(64) Position {                  PositionKey         key;               // member + client + contract    const ClientAuth*   auth;              // 客户权限(直读,不再查表)    const ContractSpec* spec;              // 合约规格:乘数 / 保证金率 / tick    const RiskParam*    risk;              // 会员级风控阈值    int64_t longVol, shortVol;             // 净头寸(基准态,见技巧 4)    int64_t margin;                        // 已占用保证金(基准态)};class PositionBook {    ankerl::unordered_dense::map<uint64_t, Position*> idx_;public:    Position* locate(const Order& o) noexcept {        auto it = idx_.find(o.key.pack());           // 整条热路径唯一的查找        if (it != idx_.end()) return it->second;     // 99.9%:直接命中        return lazyLoad(o);                          // 首次:建图,之后无限复用    }};

盘中热路径塌缩成这样:

代码 · 1
Position* p    = book.locate(order);                          // 一次 hashint64_t   need = order.qty * p->spec->multiplier *            // 乘数 / 费率都直读                 p->spec->marginRateBps / 10000;if (!p->auth->canOpen(order)) reject(order);                  // 权限也直读

启示:三次查找塌缩成一次。决策点从"每笔订单"前移到了"这个持仓第一次出现"。省下的是热路径上的内存跳转和 cache miss。

编译期计算

笨办法(决策点在盘中):

代码 · 1
int64_t margin = price * contract->multiplier * contract->marginRate / 10000;// 乘数、费率明明是常量,却每次都从内存里读、每次都重新算

聪明办法(决策点前移到编译期):

代码 · 1
struct IF2106 {                                     // 合约规格 = 编译期常量    static constexpr int64_t multiplier    = 300;    static constexpr int64_t marginRateBps = 1200;  // 12%};template <typename Contract>constexpr int64_t marginPerLot(int64_t price) noexcept {    return price * Contract::multiplier * Contract::marginRateBps / 10000;}// 实例化 marginPerLot<IF2106> 时,乘数和费率退化为立即数// 编译后只剩一条 imul + 一次移位,连内存都不用读

启示:能 constexpr 的,绝不留给运行期。决策点前移到编译期的好处是——运行时这件事根本"不存在",它已经被编译器折叠掉了。

增量计算

笨办法(决策点在"被问到时",全量遍历):每次做风控检查,都把该会员名下所有持仓的保证金重新加总一遍——持仓越多越慢。

聪明办法(决策点固定在每次成交,只吃差值):长期维护一个"会员总保证金"的累计数,每笔成交只把这次的增量加上去。读取时直接拿,永不汇总。

代码 · 1
每笔成交:会员.总保证金 += 本笔成交的保证金增量读取风控:直接读 会员.总保证金          ← O(1),无需遍历

启示:增量计算和图模型是一对孪生子——前者让"找数据"是 O(1),后者让"算汇总"也是 O(1)。热路径上,从此再无 O(N)。

上面讲的还都是"计算"的前移。但同一句口诀,能继续往更底层外推。网络组包、内存管理。。。都是一样的道理。

报文头预编码

笨办法:每发一单,都从头拼一遍"版本号 + 会员号 + 通道号 + 校验码"——可这些一整天都不会变。

聪明办法:盘前编好一个报文模板,盘中只覆盖合约 / 价格 / 数量 / 方向这些动态变化字段。

代码 · 1
盘前:填好报文模板 = {版本, 会员, 通道, 校验头}      ← 固定字段盘中:复制模板 → 只改 {合约, 价格, 数量, 方向} → 发送

和交易所的连接、登录、订阅、心跳通道,这些"重而确定"的活儿,全部在盘前做完,并做 warmup:

  • • mlockall 锁定内存页,杜绝盘中缺页;
  • • pthread_setaffinity_np 绑核独占,避免上下文切换;
  • • 配置 kernel-bypass(OpenOnload / efvi),绕过内核协议栈;
  • • 发若干"空跑"包,把网卡、PCIe、内核栈、L1 / L2 缓存全部预热。行情一到就直奔处理函数,不经过任何冷启动。连接建立这件事的决策点,被彻底前移了。

结语

回到做早餐。

那个顿悟之所以值 30 分钟睡眠,是我终于学会问一个问题:

这件事,真的非得在早上做吗?

低延迟交易也一样,其设计的本质,就是在关键路径上坚决不为信息熵为0的数据消耗哪怕1个CPU时钟周期。

要反复训练自己形成如下肌肉记忆的时间观——

遇到任何一段代码、任何一次访存、任何一个分支,先问一句:"它最早能在什么时候完成准备?" 然后把它尽量往前挪。

把能挪到编译期的,挪到编译期;把能挪到盘前的,挪到盘前;把能挪到首次访问的,挪到首次访问。只有那些"客户开口才能定"的事——价格、数量、方向——才留在最后一刻。

点击关注,共同进步

我从做早餐中悟出的低延迟系统设计之道。每天清晨给孩子做一顿热气腾腾的早餐,是一件幸福却伴随着早起痛苦的事。

查看原文 →

更进一步:量化金融体系

看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力

进入量化体系 →

相似阅读

另一事件,读法相近