1. 性能基准与度量:理解微秒级延迟、吞吐量与抖动
做量化交易系统优化,第一件事不是动手改代码。
而是搞清楚:你现在到底有多快?
我见过太多团队,上来就撸起袖子改数据结构、换内存池,结果优化完发现瓶颈根本不在那里。说白了,没有基线,你连方向都找不准。
1.1 三个核心指标:延迟、吞吐量、抖动
这三个词,做交易的人天天挂在嘴边。但真正理解它们之间关系的,其实不多。
延迟(Latency):从事件发生到系统响应的时间差。在交易系统里,通常指行情数据到达到你发出订单之间的时间。单位是微秒(μs),甚至纳秒(ns)。
吞吐量(Throughput):单位时间内能处理的事件数量。比如每秒处理多少笔订单,或者每秒处理多少条行情快照。
抖动(Jitter):延迟的波动程度。说白了,就是你的延迟稳不稳定。
我个人习惯把抖动看作「最危险的敌人」。为什么?
平均延迟 10 微秒的系统,如果偶尔跳到 100 微秒,那比一个稳定在 50 微秒的系统更可怕。因为你的策略模型根本不知道什么时候会出问题。
我在项目中遇到过一件事:某个策略在回测时表现完美,上线后却频繁亏损。查了三天,发现是 GC(垃圾回收)导致的抖动。平时延迟 5μs,GC 触发时直接飙到 500μs。嗯,这就是典型的「抖动杀人」。
1.2 如何建立性能基线
建立基线,不是跑一次测试就完事了。
你需要一个可重复的、标准化的测试流程。我建议至少包含以下几步:
- 定义关键路径:你的系统里,哪条路径最敏感?通常是行情接收 → 策略计算 → 订单发送这条线。
- 选择测量点:在关键路径上埋点,记录时间戳。注意,测量本身不能引入太大开销。
- 确定统计口径:用平均值?中位数?P99?P99.9?我个人习惯看 P50、P99 和 P99.9,这三个值能告诉你大部分情况。
- 跑足够多的样本:至少跑 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 知识体系总览
下面这张图,是我对性能基准与度量的理解框架。你可以把它当作本章的思维导图:
1.7 避坑指南
最后,分享几个我踩过的坑:
- 不要用 CPU 时间代替墙钟时间。CPU 时间不包含等待 I/O、锁竞争的时间,而交易系统最怕的就是这些。
- 不要在优化前做假设。我曾经以为数据库查询是瓶颈,结果 profiling 后发现是序列化。嗯,白忙了两天。
- 测量工具本身要轻量。如果你每次测量都要花 1μs,那你的系统延迟基线就多了 1μs 的误差。
- 别忘了冷启动效应。刚启动的系统,缓存是冷的,延迟会偏高。我一般先跑 10 万次热身,再开始正式测量。
我个人习惯把基线数据保存下来,每次改动后重新跑一遍。这样就能看到每一次优化的真实效果。说白了,就是「用数据说话」。
好了,这一章就到这里。记住:先测量,再优化。别凭感觉做事。
公众号:蓝海资料掘金营,微信deep3321