【11】hyperfine入门

2 阅读9分钟

[26/3/24]hyperfine入门

Class1 基础语法

1. 传统测试

time python3 my_inference.py

time 工具局限性:

  1. 单次运行随机性大,结果不可靠
  2. 无多次运行的统计分析
  3. 输出格式不友好,难以被其他工具解析

2. 第一次基准测试

hyperfine 'sleep 0.3'

输出解读(重要):

Benchmark 1: sleep 0.3
  Time (mean ± σ):     301.0 ms ±   0.9 ms    [User: 0.5 ms, System: 0.6 ms]
  Range (min … max):   300.0 ms … 302.5 ms    10 runs

我们需要关注这几个指标:

  • Mean (均值) : 平均耗时。这里显示 301.0 ms,非常接近我们设定的 300ms。
  • σ (Sigma, 标准差) : 代表波动范围。± 0.9 ms 说明波动很小,系统很稳定。
    • 端侧AI场景:如果你的模型推理均值是 30ms,但标准差是 10ms,说明推理极其不稳定,这在实时视频流处理中会导致卡顿,必须排查。
  • Range (极值范围) : 最小值和最大值。
  • User / System: 用户态和内核态耗时。这涉及到操作系统知识,通常对我们优化 C++ 代码有参考价值(比如是否在内核态花费了太多时间进行内存拷贝)。

3. 对比测试

这是最常用的功能。假设你想对比 ls(列出文件,不显示详细信息)和 ls -la(列出详细信息)的性能差异。

hyperfine 'ls' 'ls -la'

输出解读(只看总结部分)

Summary
  'ls' ran
    2.10 ± 0.26 times faster than 'ls -la'

关键点:

  • Summary 部分:Hyperfine 会自动计算快了多少倍。上例中 lsls -la 快了约 2.1 倍。
  • 在工作中,你可以用它来对比:./model_fp32./model_int8 的执行速度。

易错点:

  • 计算快了百分之多少是这样算的:(旧 - 新) / 旧 * 100% (时间节省比例)
  • 在终端里显示 2.10 ± 0.26 times faster than 意思是 旧 / 新 = 2.10
  • 这里用变量 N 指代 2.10,那么计算公式应该是:(1 - 1/N) * 100%
  • 本例中就是:(1 - 1/2.10) * 100% = 52.38%所以应该说运行速度快了52.38%

4. WSL 环境特别提示

不要在 /mnt/c/ (Windows 盘符) 下做严肃的性能测试!
因为 WSL 访问 Windows 文件系统需要经过协议转换,IO 延迟非常高且波动大,测出来的数据不能反映真实性能。

所以请确保在 WSL 的原生文件系统(如 ~//home/)下进行测试。

Class2 预热机制与参数化扫描

在上一课中,我们发现如果任务执行时间太短,测量结果会变得非常不稳定。而在真实的端侧 AI 开发中,我们不仅要解决“测不准”的问题,还需要批量测试不同配置下的性能。这一课将教你如何像工程师一样系统地做测试。

1. 预热机制 -w / --warmup:解决“冷启动”问题

在端侧 AI 推理中,第一次运行往往比后续运行慢很多。原因主要有两点:

  1. 模型加载与内存分配:第一次运行需要从磁盘读取模型文件、分配内存(malloc)。
  2. CPU 缓存与频率调节:程序刚启动时,CPU 可能处于低频节能状态,缓存也未命中。

Hyperfine 提供了 --warmup 参数(对应短参数为 -w,注意是小写)来解决这个问题。

指令讲解

hyperfine -w <N> 'your_command'

这表示在正式开始统计记录前,先“偷偷”运行 N 次命令,但不计入成绩。

端侧 AI 场景应用:当你测试一个 C++ 编写的推理引擎二进制文件时,如果不加 --warmup,你测到的是“程序启动时间 + 模型加载时间 + 推理时间”。加上 --warmup 后,你测到的才更接近“纯推理时间”(前提是你的程序是常驻进程或者支持热加载,对于每次都退出的二进制程序,我们后面会讲如何用脚本处理)。

2. 参数化扫描 -P / --parameter-scan:批量测试不同输入

自动修改参数跑多次测试,记录结果。Hyperfine 支持类似 Python f-string 的语法来做参数化扫描。
指令讲解

hyperfine -P para start end [-D step] 'python3 script.py {para}'

你可以用花括号 {} 来定义变量,Hyperfine 会自动遍历你提供的列表。

这里的 -P 表示参数扫描;para 是参数名字,后续会用到;start 是开始值;end 是结束值;step 是单步长。
实战演练
假设我们要模拟测试模型在不同输入尺寸下的耗时。
请运行以下命令,测试 sleep 命令在不同时长下的表现:

# 参数化扫描:delay 从 1100,步长 1
hyperfine -P delay 1 100 'sleep 0.{delay}'

# 指定步长(如每次增加 5)
hyperfine -P delay 0 100 -D 5 'sleep 0.{delay}'

# 浮点数步长
hyperfine -P delay 1.0 2.0 -D 0.1 'sleep {delay}'

# 使用Bash的序列扩展写法,请注意引号在哪
hyperfine 'sleep 0.'{1,2,3}

Hyperfine 会自动拆解为多个独立的基准测试。还会生成一个清晰的表格,展示耗时是如何线性增长的。

端侧 AI 场景应用:你可以用这个功能测试不同线程数 {1,2,4,8} 对推理速度的影响,或者测试不同分辨率 {224, 256, 512} 的处理耗时。

3. 控制运行次数 -r / --runs

有时候测试非常耗时(比如跑一个大模型推理需要 10 秒),默认运行 10 次可能太久了。

  • 可以用 --runs / -r 指定次数;
  • 或者用 --min-runs / -m 设定下限;
  • 还有用 --max-runs / -M 设定上限;
# 每个测试只跑 3 次
hyperfine --runs 3 'sleep 0.5'

Class3 导出结果(报告)

1. 把测试结果导出成 MD / JSON(写报告必备)

Hyperfine 支持把结果导出为多种格式,官方提到支持 CSV、JSON、Markdown 等。

目的是把结果整理成图表,写进技术文档或 PPT。

常用三个选项是:

  • --export-csv <file>
  • --export-json <file>
  • --export-markdown <file>

例如:测试对比同一个逻辑的代码,但是语言不同,且输出对比结果(json 的内容更加丰富)

hyperfine \
  'python3 infer.py 1000000' \
  './build/infer_cpp 1000000' \
  --export-json bench_python_vs_cpp.json \
  --export-markdown bench_python_vs_cpp.md

补充:端侧 AI 工程师日常工作:

  • 测一个模型在 TFLite / ONNX Runtime / NCNN / MNN 上的推理时间;
  • 对比不同线程数、不同输入尺寸的性能;
  • 把结果整理成图表,写进技术文档或 PPT。

Class4 进阶功能

1. 真实场景模拟--prepare:冷启动 vs 热运行

在端侧设备(如手机、嵌入式板子)上,用户对 App 的“第一次打开速度”非常敏感。这就涉及到 冷启动 问题。

  • 冷启动:系统刚启动,CPU 缓存是空的,模型文件在磁盘上不在内存中,需要从 Flash 加载。
  • 热运行:模型已经在内存里了,或者 CPU 缓存已经热了,此时推理速度最快。Hyperfine 提供了 --prepare 参数,专门用于模拟这种场景。指令讲解:
# 每次运行目标前先运行其他代码用于清除内存缓存,以此模拟冷启动
hyperfine --prepare '<command>' 'target_command'

--prepare 后面的命令会在 每一次 正式计时运行之前执行。(一般写用于清除缓存的命令)

端侧 AI 应用场景:

  • 可以写一个脚本 restart_app.sh,内容是杀掉后台进程并重新启动。
  • 然后用 --prepare './restart_app.sh' 来测试 App 每次冷启动的耗时。

2. 捕捉输出--show-output:调试与日志分析

有时候,你的推理引擎会输出一些关键信息(比如“使用了 Neon 指令集”、“检测到 NPU”等)。

使用 --show-output 可以在测试时看到这些打印,确认程序是否按预期工作。

hyperfine --show-output './build/infer_cpp 1000000'

(其实就是代码里面的 print() / std::cout 在使用 hyperfine 测试性能时默认不输出到控制台,但是使用了这个参数之后就会输出到控制台了)

注意:这会让输出变得很长,不适合做正式性能报告,但适合调试。

补充 1:真正的“冷启动”核武器(清空 Linux 缓存)

--prepare 来模拟冷启动。你举的例子是“杀掉后台进程并重新启动”。

但在 Linux 世界里,这远远不够!

Linux 会把读过的文件死死地缓存在物理内存(Cache)里。 如果只是重启 App,操作系统依然会直接从内存里把模型“秒读”出来,这叫“温启动(Warm Start)”。

真正的端侧冷启动(逼迫系统去读极慢的 Flash 存储)指令是这行内核命令:

sync; echo 3 | sudo tee /proc/sys/vm/drop_caches

实战用法:

测模型必须这样写,才能测出用户第一次点开 App 时真实的漫长等待时间:

hyperfine --prepare 'sync; echo 3 | sudo tee /proc/sys/vm/drop_caches' './infer_cpp model.bin'

(注意:清除系统缓存需要 root 权限,如果在板子上测试,可以直接用 root 账户跑)

补充 2:--setup--prepare 的生命周期差异

前面只记录了 --prepare,但 hyperfine 还有一个兄弟叫 --setup。在测试那些需要“前置数据”的 C++ 算法时,千万别搞混它们:

  • --setup '<cmd>':在整个 Benchmark 开始前,只运行一次。(比如:测试前用 Python 生成一个 1GB 的乱序测试数据文件 data.bin,生成一次就够了)。
  • --prepare '<cmd>':在每一次计时循环(Run)开始前,都会运行一次。(比如上面提到的,每次跑之前都要清空一下缓存)。

补充 3:对抗 C++ 崩溃的 --ignore-failure

端侧开发极其残酷,很多时候 C++ 代码(特别是涉及到指针和手动内存管理的)在跑几十次循环测试时,可能会偶然触发一次 Segment Fault(段错误,核心已转储)。

默认情况下,hyperfine 一旦发现你的程序崩溃了一次,就会直接罢工报错退出,导致前面跑了几十次的数据全部白费。

如果在跑极其不稳定的早期 Demo 测试,一定要加上 -i(或 --ignore-failure)参数。它会忽略那些崩溃的轮次,强行把成功的轮次耗时统计出来,这在早期抢救性能数据时是保命神技。

结语

hyperfine 只是一个工具,但它背后代表的 “控制变量、统计分析、自动化报告” 思维,才是你成为优秀工程师的关键。