1. 性能基准与度量:理解微秒级延迟、吞吐量与抖动

做量化交易系统优化,第一件事不是动手改代码。

而是搞清楚:你现在到底有多快?

我见过太多团队,上来就撸起袖子改数据结构、换内存池,结果优化完发现瓶颈根本不在那里。说白了,没有基线,你连方向都找不准。

1.1 三个核心指标:延迟、吞吐量、抖动

这三个词,做交易的人天天挂在嘴边。但真正理解它们之间关系的,其实不多。

延迟(Latency):从事件发生到系统响应的时间差。在交易系统里,通常指行情数据到达到你发出订单之间的时间。单位是微秒(μs),甚至纳秒(ns)。

吞吐量(Throughput):单位时间内能处理的事件数量。比如每秒处理多少笔订单,或者每秒处理多少条行情快照。

抖动(Jitter):延迟的波动程度。说白了,就是你的延迟稳不稳定。

我个人习惯把抖动看作「最危险的敌人」。为什么?

平均延迟 10 微秒的系统,如果偶尔跳到 100 微秒,那比一个稳定在 50 微秒的系统更可怕。因为你的策略模型根本不知道什么时候会出问题。

我在项目中遇到过一件事:某个策略在回测时表现完美,上线后却频繁亏损。查了三天,发现是 GC(垃圾回收)导致的抖动。平时延迟 5μs,GC 触发时直接飙到 500μs。嗯,这就是典型的「抖动杀人」。

1.2 如何建立性能基线

建立基线,不是跑一次测试就完事了。

你需要一个可重复的、标准化的测试流程。我建议至少包含以下几步:

  1. 定义关键路径:你的系统里,哪条路径最敏感?通常是行情接收 → 策略计算 → 订单发送这条线。
  2. 选择测量点:在关键路径上埋点,记录时间戳。注意,测量本身不能引入太大开销。
  3. 确定统计口径:用平均值?中位数?P99?P99.9?我个人习惯看 P50、P99 和 P99.9,这三个值能告诉你大部分情况。
  4. 跑足够多的样本:至少跑 100 万次以上,才能得到有统计意义的结果。

一个小技巧:测量时用 std::chrono::high_resolution_clock 或者 clock_gettime(CLOCK_MONOTONIC, ...)。别用 gettimeofday,它受系统时间调整影响,会给你假数据。

1.3 测量工具与代码示例

这里给一个我常用的 C++ 微秒级计时器。代码很简单,但很实用:

#include <chrono>
#include <vector>
#include <algorithm>

class MicrosecondTimer {
public:
    void start() {
        begin = std::chrono::high_resolution_clock::now();
    }
    
    void stop() {
        end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count();
        samples.push_back(duration);
    }
    
    void report() {
        if (samples.empty()) return;
        std::sort(samples.begin(), samples.end());
        size_t n = samples.size();
        double avg = 0.0;
        for (auto s : samples) avg += s;
        avg /= n;
        
        printf("Samples: %zu\n", n);
        printf("Avg:     %.2f μs\n", avg);
        printf("P50:     %lld μs\n", samples[n * 50 / 100]);
        printf("P99:     %lld μs\n", samples[n * 99 / 100]);
        printf("P99.9:   %lld μs\n", samples[n * 999 / 1000]);
        printf("Min:     %lld μs\n", samples.front());
        printf("Max:     %lld μs\n", samples.back());
    }
    
private:
    std::chrono::high_resolution_clock::time_point begin, end;
    std::vector<long long> samples;
};

你想想看,有了这个工具,你就能量化每一次优化到底有没有效果。而不是靠感觉说「好像快了一点」。

1.4 吞吐量与延迟的权衡

很多人以为吞吐量越高越好。其实不是。

在交易系统里,延迟往往比吞吐量更重要。你每秒能处理 100 万笔订单,但每笔要 50μs,那还不如每秒处理 10 万笔、每笔只要 5μs 的系统。

场景 延迟要求 吞吐量要求 优化重点
高频做市 < 10μs 中等 极致延迟
统计套利 < 100μs 延迟+吞吐平衡
算法交易 < 1ms 极高 吞吐量优先

我曾经犯过一个错误:为了提升吞吐量,用了批量处理的方式。结果每批要等 50μs 才凑够数量,延迟直接炸了。后来改成「来一个处理一个」,吞吐量降了 30%,但延迟降了 90%。值不值?你自己判断。

1.5 抖动:被忽视的杀手

抖动怎么测?很简单:看延迟分布。

如果 P99 和 P50 差距很大,说明抖动严重。我一般要求 P99 / P50 < 3,超过这个值就要警惕了。

注意:操作系统调度、CPU 频率切换、内存分配、甚至网卡中断合并,都会引入抖动。我曾经排查过一个抖动问题,最后发现是隔壁进程的 cron job 在整点触发了日志轮转。

所以,隔离环境很重要。能绑核就绑核,能关超线程就关超线程。

1.6 知识体系总览

下面这张图,是我对性能基准与度量的理解框架。你可以把它当作本章的思维导图:

性能基准与度量 延迟 (Latency) 吞吐量 (Throughput) 抖动 (Jitter) 平均延迟 P50 / P99 / P99.9 每秒订单数 每秒行情数 延迟分布 / 标准差 建立性能基线的四个步骤 定义关键路径 行情→策略→订单 选择测量点 埋时间戳 确定统计口径 P50/P99/P99.9 跑足够样本 ≥100万次 没有基线,就没有优化方向

1.7 避坑指南

最后,分享几个我踩过的坑:

  • 不要用 CPU 时间代替墙钟时间。CPU 时间不包含等待 I/O、锁竞争的时间,而交易系统最怕的就是这些。
  • 不要在优化前做假设。我曾经以为数据库查询是瓶颈,结果 profiling 后发现是序列化。嗯,白忙了两天。
  • 测量工具本身要轻量。如果你每次测量都要花 1μs,那你的系统延迟基线就多了 1μs 的误差。
  • 别忘了冷启动效应。刚启动的系统,缓存是冷的,延迟会偏高。我一般先跑 10 万次热身,再开始正式测量。

我个人习惯把基线数据保存下来,每次改动后重新跑一遍。这样就能看到每一次优化的真实效果。说白了,就是「用数据说话」。

好了,这一章就到这里。记住:先测量,再优化。别凭感觉做事。


公众号:蓝海资料掘金营,微信deep3321