3、性能基准测试:基准测试方法论、常用基准测试工具、如何设计有效的基准测试用例。

聊到性能调优,有个问题我经常被问到:「老张,我的系统到底快不快?」

说实话,没有基准测试,你根本回答不了这个问题。你只能凭感觉说「好像还行」、「感觉有点慢」。但感觉这东西,在量化交易里是最不靠谱的。

基准测试,说白了就是给系统拍一张「体检照」。你得知道它现在什么水平,才能谈优化。不然你改了一堆代码,到底是变快了还是变慢了?没人知道。

基准测试方法论

我个人习惯把基准测试分成三类。每一类解决不同的问题,千万别混着用。

  1. 微基准测试(Microbenchmark):测单个函数或方法的性能。比如「一个订单簿插入操作要多少纳秒」。
  2. 宏基准测试(Macrobenchmark):测完整业务流程。比如「从行情接收到策略发出信号,整个链路耗时」。
  3. 负载测试(Load Test):测系统在压力下的表现。比如「每秒处理10万笔订单时,延迟和吞吐量如何」。

核心原则:控制变量法

每次只改一个参数。我见过太多人同时改了JVM参数、代码逻辑、硬件配置,然后说「系统快了20%」。你问他到底是哪个改动起了作用?他答不上来。

嗯,这里要注意一个坑:预热(Warm-up)。JVM有JIT编译,第一次跑和第十次跑,性能可能差10倍。我曾经在项目中吃过这个亏——第一次测试结果惨不忍睹,差点把刚写的代码全删了。后来发现是没做预热。

我的习惯做法:

  • 先跑3轮预热,数据丢弃
  • 再跑5轮正式测试,取中位数
  • 不要取平均值,异常值会污染结果

常用基准测试工具

工具这东西,选对了一个顶十个。我这些年用下来,真正靠谱的就这几个:

工具 适用场景 我个人评价
JMH (Java Microbenchmark Harness) Java微基准测试 业界标准,没有之一。我所有Java性能测试都用它
Google Benchmark C++微基准测试 Google出品,靠谱。但学习曲线略陡
wrk / wrk2 HTTP接口负载测试 轻量、快速。我压测REST API首选
Locust Python负载测试 写脚本方便,适合复杂场景
sysbench 数据库/系统基准测试 测MySQL、CPU、内存都行,老牌工具

避坑指南:我曾经用JMH测一个简单的字符串拼接,结果发现StringBuilder比String.concat快了3倍。但后来发现,这个优化在整体系统中只占0.1%的耗时。你想想看,为了这0.1%去重构代码,值吗?

所以我的建议是:先做宏基准测试找到瓶颈,再对瓶颈做微基准测试。别上来就微优化。

如何设计有效的基准测试用例

设计用例这件事,我踩过的坑比走过的路还多。给你几个实战经验:

1. 场景要真实

别用「Hello World」级别的测试。你的系统在真实环境中处理的是什么样的数据?就用什么样的数据来测。

举个例子:测订单簿匹配性能,别只用10个订单。真实场景下可能有10万个订单在排队。我见过有人用100个订单测出「性能优异」,结果上线第一天就崩了。

2. 数据要有代表性

行情数据有高峰有低谷。你只测低峰期的性能,那高峰期怎么办?

我的做法是:从生产环境抓一段真实数据,包含极端行情(比如闪电暴跌、开盘瞬间)。然后用这段数据回放测试。

3. 边界条件要覆盖

空队列、满队列、单线程、多线程、缓存命中、缓存未命中……这些边界条件都要测。

我曾经在项目中遇到一个bug:当订单簿为空时,查询最优买卖价的函数返回了错误值。基准测试时没测这个场景,结果上线后第一个空订单簿就把系统搞崩了。

4. 统计指标要全面

别只看平均延迟。平均延迟会骗人。

我习惯看这几个指标:

  • P50(中位数延迟)
  • P99(99分位延迟)
  • P999(99.9分位延迟)
  • 吞吐量(TPS/QPS)
  • 最大延迟(最坏情况)

一个真实案例:

有一次我优化套利系统的信号生成模块。平均延迟从500微秒降到了200微秒,看起来很棒对吧?但一看P99,从1毫秒飙到了10毫秒。原来优化引入了偶发的GC停顿。如果只看平均值,这个优化就是「成功的」。但实际效果是灾难性的——那10毫秒的尾部延迟,让系统错过了好几次套利机会。

基准测试的完整流程

我画了一张图,把整个流程串起来。你照着这个做,基本不会出大问题。

基准测试完整流程 1. 确定测试目标 延迟?吞吐量?稳定性? 2. 设计测试用例 真实场景 + 边界条件 3. 搭建测试环境 隔离环境,控制变量 4. 执行预热 3轮预热,丢弃数据 5. 正式测试 5轮以上,取中位数 6. 收集与分析 P50/P99/P999/吞吐量 7. 优化与迭代 找到瓶颈,针对性优化 循环迭代 关键提醒 • 每次只改一个变量 • 记录所有测试参数 • 测试结果要可复现 • 不要相信直觉,相信数据 • 尾部延迟比平均延迟更重要

一个完整的JMH示例

说了这么多理论,来点实际的。这是我写套利系统时常用的JMH测试模板:

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class ArbitrageBenchmark {

    private OrderBook orderBook;
    private List<Order> testOrders;

    @Setup
    public void setup() {
        orderBook = new OrderBook();
        testOrders = generateRealisticOrders(10000);
        // 预热:先跑一遍,让JIT编译
        for (Order order : testOrders) {
            orderBook.addOrder(order);
        }
    }

    @Benchmark
    public void testOrderInsertion() {
        Order order = new Order("BTC-USDT", Side.BUY, 50000.0, 1.0);
        orderBook.addOrder(order);
    }

    @Benchmark
    public void testBestPriceQuery() {
        orderBook.getBestBid();
        orderBook.getBestAsk();
    }

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
                .include(ArbitrageBenchmark.class.getSimpleName())
                .warmupIterations(3)
                .measurementIterations(5)
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

一个小技巧:JMH的@Setup注解里,我习惯把预热逻辑也放进去。这样每次测试前数据状态都是一致的,不会出现「第一次测试数据少,后面测试数据多」的问题。

基准测试这件事,说白了就是「先测量,后优化」。没有数据支撑的优化,都是瞎折腾。我见过太多团队,花了两周优化一个函数,结果发现这个函数在整个链路里只占1%的耗时。

嗯,记住一句话:不要优化你不知道的东西。基准测试就是让你「知道」的工具。


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