胡萝卜烤地瓜
胡萝卜烤地瓜
发布于 2026-07-28 / 0 阅读
0
0

NO.A13 光跑通不够,还得跑得快-Benchmark CI

⚠️ 法律与用途声明:本文讨论 benchmark 性能量化的自动化闭环设计,基于开源工具(Conan 2 / GitHub Actions),属于经验分享。所有企业名、项目名、内部脚本与板卡型号均已脱敏,代码示例中的标识符均为占位。读者须遵守所在地区法律法规。


一、跑通了,那跑得快不快

上一篇(NO.A12)搭了上板验证闭环——构建产物到板子上跑一遍,自动判定 PASS 还是 FAIL。这回答了"跑通没跑通"。

但嵌入式 AI 还有另一层关心:跑得快不快。同一个算法,在同样一块板上,这个版本比上个版本快了还是慢了?一次推理花多少 tick?改了一行代码,性能是涨了还是跌了?

这些问题,PASS/FAIL 回答不了。你需要的是数值——不只是"跑通了",还有"花了多少时间"。这就是 benchmark 要干的事。

功能和性能是两个正交的维度。功能验证回答"对不对",性能 benchmark 回答"快不快"。一个包可能功能全对(PASS),但比上一版慢了 30%——这在嵌入式场景里可能不可接受。所以两条闭环得并行跑,各回答各的问题。

二、benchmark 不是拿功能验证的 ELF 直接跑

一个容易混淆的点:benchmark 用的 ELF,不是功能验证那个 ELF 直接拿来跑。

功能验证的 ELF 只管"跑通"——调用算法、检查返回值。benchmark 的 ELF 不一样,它要在跑算法的同时掐表计时,把每个用例的耗时记下来。所以 benchmark 是一个单独构建的产物,有自己的入口、自己的计时逻辑。

而且这个构建有个工程细节值得说:benchmark 要从一份可写的 cache 里构建,不能直接复用功能验证的 cache。原因是功能验证的构建可能是在 Docker 容器里以 root 跑的,产物文件的 owner 是 root。benchmark 如果直接在这个 cache 上构建,写入时权限对不上,报错。所以先复制出一份可写的 cache,再在里面构建 benchmark:

def build_benchmark(package_cache, target):
    # 从 run cache 复制出可写的 benchmark cache
    # (功能验证的 cache 可能是 Docker root 写的,权限冲突)
    bench_cache = copy_writable(package_cache)

    # 在独立 cache 里构建 benchmark ELF
    elf = conan_create("benchmark", bench_cache, target)
    return elf

这个"复制可写副本"的动作虽小,但很关键——它隔离了 benchmark 构建和功能验证构建的写权限,避免互相踩。和 NO.A2 讲的"即时容器不直接可写挂载模板缓存"是同一个思路:读共享、写独立

三、两种 Tick:跨平台不能比绝对值

benchmark 采集的是每个用例的耗时,以 tick 为单位。但 tick 这个词在不同平台上含义不同:

  • Cortex-M(裸机):tick 是 CPU 周期计数,由板子上的 Base Firmware 通过 SYSTICK 提供。精度极高(一个 cycle 一个 tick),但不同 MCU 主频不同,tick 的绝对含义不同。
  • Cortex-A(Linux):tick 是微秒,通过 clock_gettime(CLOCK_MONOTONIC) 采集。精度是 µs 级,和 MCU 的 cycle 不是一个量级。

这意味着一个重要约束:跨平台不能比绝对值。你不能拿 Cortex-M 上的 18432 cycles 和 Cortex-A 上的 47120 µs 直接比谁快——它们连单位都不一样。

同平台不同版本可以比相对变化。同一块 Cortex-M 板上,上一版算法跑了 18432 cycles,这一版跑了 15000 cycles,你能说这一版快了 19%。这个相对变化才是 benchmark 回归的真正价值——不是比谁绝对快,而是比自己有没有退步

四、性能数据采集:RESULT 行带值

A12 讲过统一输出协议(BENCHMARK_START / RESULT / END)。在 benchmark 场景下,RESULT 行不只表示通过,还带着测量值

BENCHMARK_START
MODULE|MyAlgo-v2.1|cases=2
RESULT|fir_n256|18432       # 不只是 PASS,还有 18432 ticks
RESULT|fft_n256|47120       # 同上
BENCHMARK_END

采集端的解析也很直接——从 RESULT 行拆出用例名和数值,存下来:

def parse_benchmark(output):
    results = []
    for line in output:
        if line.startswith("RESULT|"):
            _, name, value = line.split("|")
            results.append({
                "case": name,
                "value": int(value),
                "unit": "ticks",
            })
    return results

和功能验证的解析器长得几乎一样——因为输出协议是同一套。benchmark 只是让 RESULT 行的值从"有没有"变成了"多少"。

五、性能回归:跟历史比

benchmark 的终极价值不是某一次的数值,而是趋势——这版比上版快了还是慢了。

def perf_regression(current_results, baseline_results):
    for case in current_results:
        baseline = baseline_results.get(case.name)
        if baseline is None:
            continue   # 新用例,没历史可比

        delta = (case.value - baseline.value) / baseline.value
        if delta > REGRESSION_THRESHOLD:        # 比如超过 10%
            warn(f"{case.name} 变慢了 {delta:.1%}")
        elif delta < -IMPROVEMENT_THRESHOLD:    # 比如快了超过 5%
            info(f"{case.name} 变快了 {abs(delta):.1%}")

回归对比的关键判断是:多大的变化算退步。设一个阈值(比如 10%),超过就告警。这个阈值不能太严(MCU 上 benchmark 本身有抖动,差几个 percent 可能是噪声),也不能太松(20% 的退步你都不告,那要 benchmark 干啥)。

阈值怎么定?靠积累。跑几十次同一版代码,看数值的自然波动范围,把阈值设在波动范围之外。这是经验活,没有公式。

六、性能数据也要落盘和可追溯

和功能验证一样,benchmark 的结果也要按 NO.A11 的三键模型落盘,保证 re-run 不覆盖、attempt 可追溯:

{
  "run_key": "<run_id>-attempt-<n>",
  "target": "<目标>",
  "toolchain": "<工具链版本>",
  "benchmark_results": [
    {"case": "fir_n256", "value": 18432, "unit": "ticks"},
    {"case": "fft_n256", "value": 47120, "unit": "ticks"}
  ]
}

这份记录里每条都挂着 run_key——它是哪次构建、哪次 attempt 产出的性能数据,查得清清楚楚。后续做回归对比时,就是从这些历史记录里拉出基线值。

七、几条攒下来的判断

功能验证和性能 benchmark 是两件事,要并行跑。一个回答对不对,一个回答快不快。只跑功能不跑 benchmark,你不知道性能退步了;只跑 benchmark 不跑功能,你不知道是不是跑通了。

benchmark 产物要单独构建,不复用功能验证的 ELF。计时逻辑、入口、cache 写权限都不同,硬混在一起就是踩坑。

跨平台不比绝对值,同平台比相对变化。MCU cycle 和 Linux µs 不是一个量级,但同一块板上两版代码的 delta 是有意义的。

回归阈值要靠经验定。太严被噪声触发,太松放过退步。跑几十次摸清抖动范围,再设阈值。

性能数据也按三键落盘。run_key 隔离、attempt 可追溯,和功能验证用同一套契约。

这套 benchmark 闭环搭起来之后,你能做到一件以前做不到的事:每次代码改动,不光知道功能对不对,还自动知道性能涨了还是跌了。改一行代码,CI 自动告诉你"fir_n256 变慢了 12%"。这在以前是拿不准的——你得手动跑、手动记、手动比,通常就省略了,然后性能退了也不知道,直到客户投诉。

到这里,平台系列的上板测试板块(A11 三键模型 → A12 验证闭环 → A13 benchmark CI)就串完了。下一篇是平台系列的收束:模板化推广——怎么让前面跑通的所有能力,自动惠及每一个新仓库。


参考链接:

[1] Conan 2:交叉编译与 benchmark 构建 https://docs.conan.io/2/how-tos/cross_platform/cross_building_with_conan.html

[2] clock_gettime(Linux 高精度计时) https://man7.org/linux/man-pages/man3/clock_gettime.3.html

[3] ARM SYSTICK 定时器(Cortex-M 计时基础) https://developer.arm.com/documentation/dui0552/a/the-cortex-m3-processor/system-timer--systick

[4] 性能回归测试方法论 https://opensource.com/article/20/2/regression-testing

[5] GitHub Actions:自动化性能测试 https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-your-workflow-does/run-variations-of-tasks-using-a-matrix

[6] Conan 2:cache save / restore(benchmark 独立 cache 基础) https://docs.conan.io/2/reference/commands/cache.html


评论