Files
ViperApps/ViperSpeedBenchmark/README.md
T
TermiNexus df8e301720 chore: 初始化 ViperApps 仓库
纳入 11 个示例工程(不含 ViperOS,另行开专题)的源码:已完成新语法迁移(R1-R16 机械规则与 R1/R12 改写)。.gitignore 排除 ViperOS/、各工程编译产物(.tpv_build/.native_build/output/temp)、二进制与 ViperMNIST/data 数据集。
2026-10-07 08:11:49 +08:00

333 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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 一行):
```text
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 工程配置(产物 `.tpv_build/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`,
产物应为 `.tpv_build/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)。