跳到主要内容
微基准套件跑得慢,先别急着加机器

微基准套件跑得慢,先别急着加机器

abanana
abanana

· 阅读约 5 分钟

10 月底刷 arXiv 刷到一篇 UCC 2026 的论文,标题里挂着 microbenchmark 和 CI/CD,本来打算划过去——这类“优化测试执行效率”的题目,十条里有八条最后落到并行跑、加缓存、换更大机器上。点回来是因为摘要里有个我没料到的数字:Java 项目上省 42% 总执行时长。

真正让我把 PDF 存下来的是同一句话的后半段:调度开销低于总运行时间的 0.001%。

前半句是卖点,后半句才是这类东西能不能进 CI 的分水岭。在线调度最怕的就是调度本身比它省下来的还贵,比例一旦压到千分之一以下,讨论“值不值得引入”就变成了讨论“实现得对不对”。我见过太多“智能调度”最后死在决策开销上,所以看到这个数量级先愣了一下。

它在改的是顺序,不是机器

问题的形状大家都熟。微基准套件一轮跑下来时间不短,而现在的发布节奏本来就往上走,agent 帮忙写代码之后只会更密,每次提交都触发全量跑,等结果的时间比写代码还长。于是常见的处理是三选一:砍掉一部分用例、挪到 nightly、或者跑一遍红了手动重跑。第三种我见得最多,本质上是在用运气替换统计。

AdapBench 的想法是把这轮执行当成一个在线决策过程,每跑完一个基准就更新一次手上的证据,然后决定下一个跑谁、以及要不要现在就停。按论文的说法,它是第一个在运行时同时做这两件事的——动态排序 + 逐个提前停止。流程大意是这样,注意这不是论文原文,是我看完描述自己画的:

pending = 全部基准
while pending:
    b = argmax(信息量(b) for b in pending)   # 先跑最可能翻盘的那个
    run(b)
    pending.remove(b)
    if 证据足够接受/拒绝这次变更假设:
        break                                # 剩下的不跑了

看着朴素,难点全在 信息量 和 证据足够 这两个谓词上。每多跑一个基准就多一次假设检验,假阳性会累积;而你要在样本量还没定下来的时候就下结论,等于在跑的过程中做检验——这套东西在统计上有个专门的坑,我第一遍读的时候把它想简单了,以为“跑够 N 次就停”和“证据够了就停”差不多,后来才想明白差别在于前者根本不知道自己在检验什么,后者得随时维护一个能用来做决策的后验。

论文里那句“变更检出率和假阳性率与统一跑全量都匹配到 90%”,我理解是它在给自己找一个锚:不是另立一套宽松标准,而是把结论可靠性对齐到全量跑的水平上,再看能省多少。这个锚定方式挺关键,否则省下来的时间就是从结论可信度里抠出来的,账面上好看,实际是把回退放进了生产。

那 0.001% 比 42% 更值得看

把数字摆一起大概是这样:

Java(8 个项目,检出率/假阳性率对齐 90%)
  全量顺序跑     100%
  AdapBench      约 58%
  调度开销       < 总时长的 0.001%

Go(4 个项目)
  全量顺序跑     100%
  AdapBench      约 15%
  假阳性         0

一个能省 60% 但调度开销 5% 的方案,在长套件上也许比这个划算,但在短套件上一定是灾难——套件跑 40 分钟,光调度吃掉 2 分钟,谁也不愿意。所以我现在看这类系统会先翻调度开销那一栏,再看节省比例,顺序反过来的话很容易被漂亮的百分比带跑。

💡 小技巧:如果你也想在自己 CI 里试类似思路,第一步不是写调度器,是先把那条 benchmark job 的历史时长和波动拉出来看一眼。波动本身就比套件长度大得多的项目,提前停止基本没有意义,先治噪声。

留个坑:Go 那边为什么差这么多

85% 对 42%,差得有点过分。零假阳性这个结果也太干净,我第一反应是“套件规模不一样吧”,但摘要里没给,我也没去正文逐段抠。可能的解释有好几种——项目数少、变更检出率的基线不同、Go 那边的基准噪声结构本来就不一样——但我现在确实说不准,这条先记下来了,下次翻全文再补。

顺便,8 个 Java 加 4 个 Go,11 页、3 图、3 表。这个实验规模在同类论文里算中等偏小。这类系统最怕的情况就是换个项目就不灵,我持保留态度,但保留归保留,思路本身是能借的。

我打算怎么用

不装 AdapBench。它的原型是 Java/Go 的,我们那条流水线不是这个技术栈,硬接成本比收益高。但我可以先做一个粗糙版本验证一下假设:按历史失败率排序,加一个最短证据窗口,跑够几轮才允许停。

# 我自己草拟的,不是 AdapBench 的接口,别照抄
benchmark:
  order: by_historical_flake_rate
  early_stop:
    min_rounds: 3          # 证据不足不许停
    max_rounds: 10
  on_timeout: report_only  # 先别拦合并

第三步的 report_only 是故意的。先跑两周只看数据不拦人,把误报率摸出来,再决定要不要让它真的卡住合并。之前有次我图省事直接让一个新加的门禁拦合并,结果头一天拦下的全是它自己判断错了,那天下午基本在给它擦屁股。

划重点:第一,先看调度开销再看节省比例,顺序反过来容易被百分比忽悠;第二,“证据够了就停”和“跑够次数就停”不是一回事,前者要求你随时清楚自己在检验哪个假设;第三,想验证这类思路,先量自己套件的噪声底,波动大的套件上做提前停止是白费劲。这条记下来了,下次应该用得上。你要是也在 CI 里跟慢吞吞的基准套件较劲,可以照上面那个粗糙配置先跑两周,摸出来的数据比论文里的百分比对你更有用。

abanana
abanana

把自己踩过的坑整理成一篇能复现的笔记,写给三个月前的自己看。

查看主页 →