# ViperSpeedBenchmark "同内核多实现测速 + 组间差值核验" 基准测试工程(2026-09 修复版)。 在同一数学内核上对比 **Viper-SIMD / Viper-Scalar(同一 exe 的"向量化 vs 未向量化"对照) / 手写 C / 手写 C++ / Python** 五个实现, 按"进程内多次 run = 一组 × 5 组(默认; 首组仅汇报不计入统计)"记录原始数据、 做组间差值核验,每次 run 的运算量统一为 **≈ 2.147e9 flops**。 ## 1. 被测内核与负载(四语言一致) ```text 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 ` 行回报; `-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 一行): ```text CFG AFFINITY # -1 = 绑定失败 TSC # 入口标定的 TSC 频率; 0 = 标定失败(该实现周期计数未推进) RESULT # flops = (LEN*ITERS*2) / 墙钟秒 CHECK # 全部结束后, 最后一次 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 `), 任何启动/解析异常打印完整 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) ```bat python run_bench.py ``` 常用参数: ```bat :: 只跑部分实现 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 与之同规模、通常慢 1~3 倍; 纯 Python 回退 (无 numpy)会把趟数自动降为 8, 单 run 约 0.2~0.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 张图 ```bat 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)。