ViperSpeedBenchmark
"同内核多实现测速 + 组间差值核验" 基准测试工程(2026-09 修复版)。
在同一数学内核上对比 Viper-SIMD / Viper-Scalar(同一 exe 的"向量化 vs 未向量化"对照) / 手写 C / 手写 C++ / Python 五个实现, 按"进程内多次 run = 一组 × 5 组(默认; 首组仅汇报不计入统计)"记录原始数据、 做组间差值核验,每次 run 的运算量统一为 ≈ 2.147e9 flops。
1. 被测内核与负载(四语言一致)
double 向量, LEN = 16384 (2^14, 4 的倍数)
输入 a / b / c 用固定种子伪随机(LCG, minstd 参数)填充一次, 三语言逐位一致
out[i] = a[i] * b[i] + c[i] # 元素独立, 单趟融合乘加; 每元素 2 次浮点运算
每次计时 run = ITERS = 65536 趟内核 -> 总 flops = LEN*ITERS*2 = 2147483648 ≈ 2.147e9
-
工作集刻意压在私有 L2 之内(512 KiB):
a/b/c/out四份共 512 KiB, 本机 (AMD Ryzen AI 7 H 350)每核 1 MiB 私有 L2, 所以访存不落到共享 L3 / DRAM —— 不再被其它进程抢带宽, 这是消除"零星高时延尖峰"的关键。LEN × ITERS × 2恒为 ≈2.147e9 flops/run, 所以 GFLOPS 与旧版(DRAM 级工作集)可直接对比。 再往下压到 L1(需LEN ≤ 1024)会让每趟的内核调用/派发开销占比飙升, 对 Python+numpy 尤其致命(实测每趟固定开销约 0.4 µs)。 -
LCG 公式(所有实现逐位一致):
state = fmod(state * 48271.0, 2147483647.0); val = state / 2147483647.0种子固定12345.0。C 用math.h::fmod, Viper 用vipermath.fmod, Python 用math.fmod(取值范围内逐位一致)。填充顺序统一为逐元素a[i], b[i], c[i]依次消耗同一条 LCG 流。 -
Viper = 单趟真硬件 FMA: 在
vipersimd新增批量内核simd_fma_array(a, b, c_val, out, n)(仿照simd_mul_array的 ×8 双缓冲 AVX2 ymm 循环, 每大迭代 16 路 load 在飞后 8 条vfmadd213pd一次完成out=a*b+c, 加数 c 直接用内存操作数) —— 不是旧版mul→tmp→add的两遍写法, 无中间数组、 无 1.5 倍内存流量(见includes/vipersimd.vp)。 -
Viper 原生对照内核(SIMD 加速比的基准线): 每个操作都有一对"SIMD / 未向量化" 实现, 两侧用的是对应宽度的同一条 IEEE 指令, 因此逐元素结果逐位一致, CHECK 可直接对照:
mode 操作 SIMD 实现 原生(标量)实现 标量指令 0 / 1 FMA a*b+csimd_fma_arrayscalar_fma_arrayvfmadd213sd2 / 3 ADD a+bsimd_add_arrayscalar_add_arrayvaddsd4 / 5 MUL a*bsimd_mul_arrayscalar_mul_arrayvmulsd6 / 7 SQRT sqrt(a)simd_sqrt_arrayscalar_sqrt_arrayvsqrtsd同一个
app.exe靠命令行第 4 个参数mode选内核, 除"是否向量化"外没有任何其它 差异;run_bench.py把它们记成viper* / viper_scalar*两组数据, 供plot_bench.py图1 画 ADD/MUL/SQRT 的延迟对比与加速比。ADD/MUL/SQRT 每元素只有 1 次浮点运算(FMA 是 2 次), 所以它们的单 run flops 也 如实按 1 计, 耗时约为 FMA 的一半 —— 图1 比的是"同操作下原生 vs SIMD"的倍数, 不受这个差异影响。
-
CHECK 舍入口径(2026-09 起 C/C++ 也开 FMA): C/C++ 与 Viper 都走 单趟真硬件 FMA —— C/C++ 编译加
-mfma -ffp-contract=fast, 让a[i]*b[i]+c[i]收缩成一条vfmadd, 逐元素结果是单次舍入, 与 Viper 的simd_fma_array同口径, 两者 CHECK 应当逐位一致; Python+numpy 仍是multiply→tmp→add两次舍入, 与前三者可能差 1 ulp。各实现内部自洽 (每次进程 CHECK 恒定), 但 numpy 与原生实现不逐位相等属预期。 -
线程绑定(每个实现必做): 进程入口调
SetThreadAffinityMask把自身绑到 第一个可用逻辑核(从1<<0逐个试, 第一个被系统接受的位), 并以AFFINITY <core>行回报;-1= 绑定失败(不致命)。各实现会绑到同一个核, 消除跨核迁移、大小核异构与缓存失效带来的抖动, 横向对比才有意义。 -
双计时(每次 run 同时采集两个数):
字段 来源 特性 wall_usQueryPerformanceCounter墙钟; 含被抢占 / 被降频的时间, 对调度敏感 cpu_usQueryThreadCycleTime只计本线程真正执行过的周期; 不含被抢占时间, 对降频也免疫 TSC 频率由各实现入口自行标定: 绑核之后忙等 30ms 数周期, 重复 3 次取最大 (取最大是为了压掉"标定窗口内被抢占导致 hz 偏小"的系统性正偏置 —— 该偏置会让
cpu_us偶尔比wall_us还大, 看着像 bug)。 -
为什么不用
GetThreadTimes: 它的 CPU 记账粒度跟随调度时钟节拍, 本机实测 硬地板 15.625 ms —— 一个 ~37ms 的 run 只能被记成 2 或 3 个节拍 (31250 / 46875 µs), 误差 ±40%, 连"CPU 时间 > 墙钟时间"这种物理上不可能 的结果都会出现。已验证timeBeginPeriod(1)在 Win10 2004+ 无法改善该粒度 (加完后值仍是 15625 的整数倍)。QueryThreadCycleTime的粒度是周期级。 -
统一输出协议(每个可执行进程开头:
CFG/AFFINITY/TSC各一行; 之后每次 run 一行):CFG <len> <iters> AFFINITY <core> # -1 = 绑定失败 TSC <hz> # 入口标定的 TSC 频率; 0 = 标定失败(该实现周期计数未推进) RESULT <wall_us> <cpu_us> <flops> # flops = (LEN*ITERS*2) / 墙钟秒 CHECK <sum> # 全部结束后, 最后一次 run 的 out 升序累加 -
线程优先级: 四个实现绑核后统一调
SetThreadPriority(THREAD_PRIORITY_HIGHEST), 压低背景进程抢占造成的"零星高时延尖峰"。 -
Viper 侧为什么用
OpenThread而不是GetCurrentThread(): 后者返回伪句柄(HANDLE)-2(负指针)。在 C / Python 里它只是一段裸位模式, 可以原样传下去; 但 Viper 的t.CPtr语义下传递不可靠 —— 实测QueryThreadCycleTime只写回 约0.230 µs(≈ 一次调用开销的周期数), 即计数器根本没被读出来。 改用GetCurrentThreadId()+OpenThread()拿真句柄, 全程t.CSizeT表达, 彻底绕开指针/NULL 语义。
2. 修复内容(相对旧版 bench_report 暴露的问题)
| 旧问题 | 本次修复 |
|---|---|
| 噪声过大 → 差值核验全 FAIL | 旧版"每次 run 一次冷进程"; 现每组=一次进程启动, 进程内先 2 轮满负荷热身(每轮 = ITERS 趟 + 读回 out 校验)再连续计时 N 次, 消除冷页/启动/频率爬升噪声 |
| Python 段缺失 | run_bench.py 默认各实现都跑(py 用 sys.executable bench_py.py <runs>), 任何启动/解析异常打印完整 traceback |
| vipersimd 力度不足 | 弃用 simd_mul_array+simd_add_array 两遍, 新增/调用 simd_fma_array 单趟 FMA(×8 ymm 双缓冲, 3 输入 1 输出全走寄存器/访存流水) |
| 运算量太小 | LEN=16384 × ITERS=65536 × 2 ≈ 2.147e9 flops/次 run(旧版约 0.5e6) |
| 访存打到共享 L3 / DRAM, 被其它进程抢带宽 → 零星高时延尖峰 | 把工作集压到 512 KiB(四份缓冲合计), 稳居每核私有 L2 之内; 同时绑核 + 提线程优先级 |
| 跨核迁移 / 调度噪声无法定位 | 每个实现入口 绑定固定逻辑核(SetThreadAffinityMask), 并双计时: 墙钟(QPC) + 线程 CPU(QueryThreadCycleTime)。二者差额即被抢占/被降频吃掉的部分 |
| 离群样本把组均值拉偏 | 组内先按中位数上界裁剪(--trim-ratio, 默认 1.2)再统计; 并提供 --stat median/geomean 应对双峰分布 |
| C/C++ 与 Viper 舍入口径不一致 | C/C++ 放开 FMA 收缩(-mfma -ffp-contract=fast), 与 Viper 的 simd_fma_array 同为单趟硬件 FMA 单次舍入 |
3. 文件清单
| 文件 | 说明 |
|---|---|
project.vpj |
Viper 工程配置(产物 __vpcbuild__/output/app.exe) |
App/main.vp |
Viper 实现: 8 个内核(FMA/ADD/MUL/SQRT × SIMD/原生) + 线程绑定 + QPC/周期双计时 + 进程内多 run; 自解析命令行 [runs] [len] [iters] [mode](GetCommandLineA, 不依赖 sys/memhub) |
bench_c.c |
手写 C: 裸数组 + 全指针 __restrict(fill/内核/校验和/local 指针都加) + noinline, -O2 -mavx2 -mfma -ffp-contract=fast 自动向量化单趟 FMA |
bench_cpp.cpp |
手写 C++: std::vector + __restrict, 同口径(-O2 -mavx2 -mfma -ffp-contract=fast) |
bench_py.py |
Python: 优先 numpy(就地 ufunc 两次舍入, 与 C/C++/Viper 的 FMA 单次舍入可能差 1 ulp); 无 numpy 自动纯 Python 降规模回退(趟数=8, 如实打印 CFG)。线程绑定与线程 CPU 时间走 ctypes 调 kernel32 |
run_bench.py |
总编排: 每组一次子进程 + 解析 CFG/MODE/AFFINITY/TSC/RESULT/CHECK + 离群裁剪 + 分组统计与差值核验 + 多尺寸扫描 + 控制台/bench_report.txt/CSV + --flops-check。Viper 侧 8 个内核变体(viper/viper_scalar/viper_add/viper_scalar_add/viper_mul/viper_scalar_mul/viper_sqrt/viper_scalar_sqrt, 同一个 exe 只差 mode) + c / cpp / py |
plot_bench.py |
读 run_bench 导出的 CSV 出 4 张图(matplotlib): ①左=ADD/MUL/SQRT 原生 vs SIMD 延迟、右=各操作加速比 ②各实现耗时折线(叠加每次 run 实测点) ③Overall Mean 耗时对比 ④Measured GFLOPS 对比; 另附控制台文字小结 |
bench_report.txt |
最近一次运行日志(由 run_bench.py 生成) |
../../includes/vipersimd.vp |
新增批量内核 simd_fma_array(单趟融合乘加, ×8 ymm 双缓冲) 与 4 个原生对照内核 scalar_fma_array / scalar_add_array / scalar_mul_array / scalar_sqrt_array(逐元素标量指令, 与对应 SIMD 版逐位同结果) |
4. 前置条件
- Viper 工具链: 可构建
project.vpj(见 §5.1)。 - C/C++ 工具链(默认):
D:/msys64/mingw64/bin/clang.exe与clang++.exe; 其它目录用--cc-dir指定。 - Python 3.8+(numpy 可选, 有则全规模 2.147e9 flops; 无则纯 Python 回退)。
- CPU 需支持 AVX2 + FMA(vipersimd 与 C/C++ 的
-mavx2 -mfma均使用 YMM/FMA)。
5. 如何构建与运行
5.1 构建 Viper 部分(一次性)
在本工程目录(ViperSpeedBenchmark/)用你惯用的 tpv 方式构建 project.vpj,
产物应为 __vpcbuild__/output/app.exe。若未构建, run_bench.py 会提示并跳过
全部 Viper 内核变体 继续其它实现。
改了
App/main.vp或includes/vipersimd.vp后必须重建, 否则任一 Viper 内核变体都会因为产物回报的MODE与期望不符(旧 exe 不认命令行第 4 个参数、 或没有新内核)而显式报错 —— 这是有意为之: 宁可失败, 也不让某个内核的数据被 另一个内核冒充, 污染 SIMD/原生 对照。
5.2 运行完整基准(默认: 8 个 Viper 内核变体 + C/C++/Python, 共 11 个; 每组 = 一次进程, 进程内 10 次 run)
python run_bench.py
常用参数:
:: 只跑部分实现
python run_bench.py --impl c,cpp
:: Viper 的 SIMD vs 原生 对照(同一个 app.exe, 只差命令行 mode 参数)
python run_bench.py --impl viper,viper_scalar --sizes 1024,4096,16384
:: 只要图1 的内核成对数据(默认已含它们, 这里只是把范围收窄)
python run_bench.py --impl viper_add,viper_scalar_add,viper_mul,viper_scalar_mul,viper_sqrt,viper_scalar_sqrt
:: 只要图2/3/4 的跨语言对比(跳过 6 个 ADD/MUL/SQRT 变体)
python run_bench.py --impl viper,viper_scalar,c,cpp,py
:: 快速冒烟(减少组数/每组次数; 每次进程仍做 2 轮满热身)
python run_bench.py --impl c,cpp,py --groups 2 --runs-per-group 3
:: 自定义容差 / 导出原始数据 CSV / 启用 flops 核验
python run_bench.py --tol 0.05 --csv out.csv
python run_bench.py --flops-check
:: 多尺寸扫描: LEN 决定工作集(四份缓冲共 LEN*32 字节), ITERS 自动配平
python run_bench.py --sizes 1024,4096,16384,65536,262144
:: 抗噪口径: 用线程 CPU 时间当主指标 + 中位数 + 裁剪
python run_bench.py --metric cpu --stat median --trim-ratio 1.2
:: 关闭离群裁剪(回到"全部样本计入")
python run_bench.py --trim-ratio 0
:: Python 模式强制
python run_bench.py --impl py --py-mode pure
python run_bench.py --impl py --py-mode numpy
:: 自定义 clang 目录 / 自定义 viper 产物
python run_bench.py --cc-dir C:/msys64/mingw64/bin
python run_bench.py --viper-exe D:/path/to/app.exe
C/C++ 源码更新(或编译选项变化)后会自动重编到 .native_build/。
单 run 期望时长: 每次 run ≈ 2.147e9 flops, 工作集 512 KiB 常驻 L2。实测吞吐
取决于机器: 原生(C/C++/Viper, 单线程 AVX2+FMA)大致 几十 ms 量级;
Python+numpy 与之同规模、通常慢 13 倍; 纯 Python 回退
(无 numpy)会把趟数自动降为 8, 单 run 约 0.20.5 s。完整默认跑 11 个实现 × 10 进程,
墙钟约 1~5 分钟, 快速验证请用 §5.2 冒烟参数。
5.3 输出与日志
- 控制台实时输出; 结束后全文写入
bench_report.txt。 --csv out.csv: 逐样本原始行, 列 =impl, len, iters, group, run, wall_us, cpu_us, flops, check, trimmed(trimmed=1表示该样本被离群裁剪剔除, 未计入该组的组统计量)。--flops-check: 核对各实现 CFG 单 run 总运算量是否 ≈ 2.147e9, 并逐 RESULT 行 验证flops字段与us换算一致(flops = CFG_total / 秒); 对纯 Python 降规模配置给出提示。注意flops是吞吐, 只有单 run 时长恰 ≈1 s 时数值才 ≈2e9, 报告会同时给出实测 GFLOPS。
6. 多尺寸扫描与出图
6.1 --sizes: 一次跑出"吞吐 vs 工作集"曲线
--sizes 接受逗号分隔的 LEN 列表。只调 LEN, ITERS 自动配平(ITERS = 2^30 / LEN),
所以每次 run 的运算量恒为 ≈2.147e9 flops, 各尺寸的 GFLOPS 可直接横向比较。
工作集 = a/b/c/out 四份合计 = LEN × 32 字节:
| LEN | ITERS | 工作集 | 落在 |
|---|---|---|---|
1024 |
1048576 | 32 KiB | L1D(本机 48 KiB) |
4096 |
262144 | 128 KiB | L2 |
16384 |
65536 | 512 KiB | L2(默认, 本机每核 1 MiB) |
65536 |
16384 | 2 MiB | L3(本机 16 MB 共享) |
262144 |
4096 | 8 MiB | L3 / 边缘 |
1048576 |
1024 | 32 MiB | DRAM |
LEN 必须是 4 的倍数(内核按 4-lane ymm 分批), 且必须让 2^30 / LEN 整除;
不满足时 run_bench.py 直接报错退出, 不做静默修正。
想看"纯算力"(SIMD 能力)看小尺寸那一端, 想看"访存带宽"看大尺寸那一端。 本机四个实现应该都会在 512 KiB~2 MiB 附近出现一个峰(私有 L2 内), 再加大就掉到共享 L3/DRAM 上 —— 曲线拐点就是这台机器各级缓存的边界。
6.2 自动导 CSV
run_bench.py 默认自动导出 CSV(带时间戳, 如 bench_20260910_203105.csv),
不用再加 --csv。要指定文件名用 --csv name.csv, 完全不写用 --no-csv。
CSV 列: impl, len, iters, group, run, wall_us, cpu_us, flops, check, trimmed
(trimmed=1 表示该样本被离群裁剪剔除)。
6.3 plot_bench.py: 从 CSV 出 4 张图
python plot_bench.py :: 自动取最新的 bench_*.csv
python plot_bench.py bench_20260910_203105.csv
python plot_bench.py --metric cpu
python plot_bench.py bench_xxx.csv --out-dir figs --prefix run1
python plot_bench.py --simd-impl viper --scalar-impl viper_scalar
产出 4 张 PNG(默认与 CSV 同目录, 文件名前缀 = CSV 名):
<前缀>_1_simd_vs_scalar.png—— 左右双面板:- 左: 每种操作(ADD / MUL / SQRT)一组柱, 组内左=原生(标量)、右=ViperSIMD, 柱顶标该操作的每 run 均值(us)
- 右: 同一批操作的 SIMD 加速比(原生延迟 ÷ SIMD 延迟), 柱顶标
xN.NN, 并画一条x1 = no gain参考线 前提: CSV 里要同时有这三种操作的 原生+SIMD 成对数据(见 §5.2 的 "图1 ... 内核成对数据"); 缺哪一对就少画哪一根, 全缺则整张图跳过并提示。只取 CSV 里最小的那个 LEN。
<前缀>_2_exec_time.png—— Execution Time(microseconds): 每实现一条均值折线, 并把每组内每次 run 的实测点画成同色散点(各实现横向错开 以免重叠)。多尺寸时横轴 = LEN(折线即"工作集-耗时曲线"); 只跑了 1 个 LEN 时 横轴自动改用组号(每组 = 一次进程内的多次 run), 否则单点连不成折线。 动态范围 >30 倍时纵轴自动转对数(--y-scale log|linear)。<前缀>_3_overall_mean.png—— Overall Mean Execution Time: 每实现一根柱(全部尺寸、全部组有效样本合并平均), 误差棒 = 样本标准差, 柱顶标绝对耗时, 柱身标"相对最快"倍数。<前缀>_4_gflops.png—— Measured GFLOPS Throughput: 每实现一根柱(GFLOPS = LEN×ITERS×2 ÷ 单 run 秒数, 逐样本算后取平均), 误差棒 = 样本标准差, 柱身标"相对最快"倍数。
三张柱/线图都跟随 --metric(wall 默认 QPC 墙钟 / cpu 线程 CPU 时间),
且与 run_bench.py 报告同口径: CSV 里 trimmed=1 的离群样本不计入。
同时在控制台打一份文字小结(impl / LEN / 工作集 / 每run耗时 / GFLOPS / CPU比 / trimmed)。
依赖
matplotlib+numpy。图内文字一律用英文(短句 + 多行换行, 避免长句把标题 撑爆); 控制台小结仍为中文。字体已显式配置Microsoft YaHei, 万一要出中文图也不会变方块。
7. 统计口径与差值核验(默认容差 5%)
每个实现(impl)每次 run 产出 两个原始值(wall_us 与 cpu_us); 一组 = 一次
进程启动内的 runs_per_group(=10)次连续计时。
7.1 主指标、离群裁剪、统计量
- 主指标
--metric(默认wall): 决定组均值 / 总体 / 差值核验用哪个字段。 另一指标同时采集, 在报告的"对照"行与最终汇总里一并给出, 也都写进 CSV。 被抢占较多的机器上建议试试--metric cpu。 - 离群裁剪
--trim-ratio(默认1.2): 每组先按组内中位数剔上界离群点 —— 只保留值 <= 组内中位数 × 1.2的样本, 再算该组统计量。<=0关闭裁剪。 - 统计量
--stat(默认mean):mean(裁剪后算术平均) /median/geomean。 报告里三个值同时列出, 不必重跑。双峰分布(一半样本被降频或被抢占)下median的抗噪能力远强于算术平均。
7.2 各统计量的定义
对每个实现:
- 组统计值: 组内 10 次原始值先经离群裁剪, 再按
--stat取统计量 (默认 = 裁剪后算术平均)。报告同时列出该组的 均值 / 中位数 / 几何均值。 - 组内差(max-min): 裁剪后保留样本的最大值 - 最小值(主要反映进程内稳态抖动)。
- 总体统计值: 该实现全部
统计组数 × runs_per_group个裁剪后样本按--stat统计(默认排除首组后)。 - 样本标准差 / 最大偏差: 裁剪后样本的 n-1 无偏标准差;
|样本-总体|最大者。 - 组 vs 总体差值:
组统计值 - 总体统计值(绝对 us 与相对百分比)。 - 相邻组差值:
组统计值[g+1] - 组统计值[g](绝对 us 与相对百分比)。 - 差值核验: 每组
|组统计值-总体|/总体 <= tol且|相邻组差值|/总体 <= tol→PASS, 否则列出违规组 →FAIL。默认 tol = 0.05,--tol可调。
首组效应规避(默认): 组1(第一次进程启动)在报告中仍逐组/逐 run 展示, 但不计入总体均值、样本标准差、最大偏差与差值核验(含组1->组2 相邻差), 用于剔除冷启动/频率爬升阶段的偏差。需要把组1纳入统计时加
--include-first-group。
扰动缓解说明: (1) 每个进程入口 绑定到固定逻辑核(
AFFINITY), 消除跨核迁移与 大小核异构; (2) 每个进程内先 2 轮满负荷热身(每轮 = ITERS 趟内核 + 读回 out 求校验和), 缓存/TLB/频率进入稳态后才开始计时; (3) 组内多次 run 取自同一进程, 没有冷启动开销; (4) 每次 run 同时给出墙钟与线程 CPU 时间, 二者的差额就是 被抢占 / 被降频吃掉的部分 —— 若墙钟抖动大而 CPU 时间稳, 说明是调度噪声而非代码问题; (5) 组内离群点先按中位数上界裁掉, 双峰分布下可用--stat median进一步抗噪; (6) 尺寸扫描时 LEN×ITERS 恒定(单 run 运算量恒为 ≈2.147e9 flops), 只让工作集变化, 组间只剩真实抖动。
8. 防止"优化器把循环删掉"与已知注意点
- C/C++: 内核 noinline + 全指针
__restrict(C 与 C++ 统一拼写, 不再一个用 C99restrict一个用__restrict); 每轮热身与每次 run 之后 升序读回out求和(计时区外), 使计时区内每条写可观察、阻止跨 run 写折叠; 未开 fast-math, 但显式开-ffp-contract=fast+-mfma, 允许收缩成单趟 FMA (与 Viper 同口径、同一舍入次数)。要复现旧的"两次舍入"口径做 A/B, 把run_bench.py里的NATIVE_FLAGS改回-ffp-contract=off并去掉-mfma即可。 - Viper:
simd_fma_array内联汇编自带 memory clobber, 计时区外同样读回校验。 - Python: numpy 就地 ufunc / 纯 Python 循环都不存在被删问题; 纯 Python 回退按
实际趟数打印 CFG(≠2e9 配置),
--flops-check会给出提示。 - Viper 与 C/C++ 现在同为单次舍入 FMA, CHECK 应当逐位一致; numpy 是两次舍入, 与它们 可能差 1 ulp(见 §1), 跨实现比较 CHECK 数值前先确认这一点。
- 线程 CPU 时间(
cpu_us)的适用边界: 它由QueryThreadCycleTime的周期数除以 入口标定的 TSC 频率得到。现代 x86 的 TSC 是 invariant 的(与核心实际频率无关), 所以该值就是"本线程真正跑过的时间"; 但在极老的非 invariant TSC 机器上, 周期数会 跟着核心频率走, 换算出的cpu_us会失真 —— 那种机器上请以--metric wall为准。 另外标定本身有约 0.1% 的残差(已用三次取最大压掉大部分), 不影响各实现之间的横向对比。 - 建议空闲机器上跑完整基准; CPU 需支持 AVX2+FMA(见 §4)。