[26/3/24]hyperfine入门
Class1 基础语法
1. 传统测试
time python3 my_inference.py
time 工具局限性:
- 单次运行随机性大,结果不可靠
- 无多次运行的统计分析
- 输出格式不友好,难以被其他工具解析
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 会自动计算快了多少倍。上例中
ls比ls -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 推理中,第一次运行往往比后续运行慢很多。原因主要有两点:
- 模型加载与内存分配:第一次运行需要从磁盘读取模型文件、分配内存(malloc)。
- 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 从 1 到 100,步长 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 只是一个工具,但它背后代表的 “控制变量、统计分析、自动化报告” 思维,才是你成为优秀工程师的关键。