Files

22 KiB
Raw Permalink Blame History

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+c simd_fma_array scalar_fma_array vfmadd213sd
    2 / 3 ADD a+b simd_add_array scalar_add_array vaddsd
    4 / 5 MUL a*b simd_mul_array scalar_mul_array vmulsd
    6 / 7 SQRT sqrt(a) simd_sqrt_array scalar_sqrt_array vsqrtsd

    同一个 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_us QueryPerformanceCounter 墙钟; 含被抢占 / 被降频的时间, 对调度敏感
    cpu_us QueryThreadCycleTime 只计本线程真正执行过的周期; 不含被抢占时间, 对降频也免疫

    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. <前缀>_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. <前缀>_2_exec_time.png —— Execution Time(microseconds): 每实现一条均值折线, 并把每组内每次 run 的实测点画成同色散点(各实现横向错开 以免重叠)。多尺寸时横轴 = LEN(折线即"工作集-耗时曲线"); 只跑了 1 个 LEN 时 横轴自动改用组号(每组 = 一次进程内的多次 run), 否则单点连不成折线。 动态范围 >30 倍时纵轴自动转对数(--y-scale log|linear)。
  3. <前缀>_3_overall_mean.png —— Overall Mean Execution Time: 每实现一根柱(全部尺寸、全部组有效样本合并平均), 误差棒 = 样本标准差, 柱顶标绝对耗时, 柱身标"相对最快"倍数。
  4. <前缀>_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++ 统一拼写, 不再一个用 C99 restrict 一个用 __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)。