# -*- coding: utf-8 -*- # ============================================================ # ViperSpeedBenchmark - Viper(vipersimd) 进程内多次 run 基准实现 # # 内核(与 bench_c.c / bench_cpp.cpp / bench_py.py 一致): # out[i] = a[i] * b[i] + c[i] i = 0 .. LEN-1, LEN = 16384 (double) # # 每次计时 run = ITERS = 65536 趟内核, 单趟 2 次浮点运算/元素, # 总 flops = LEN*ITERS*2 ≈ 2.147e9(与 C/C++/Python 完全同规模)。 # # SIMD 实现: vipersimd.simd_fma_array —— 单趟融合乘加批量内核 # (×8 双缓冲 AVX2 ymm0-15, 每大迭代 16 路 load 在飞后 8 条 # vfmadd213pd 一次完成 out=a*b+c, 加数 c 直接用内存操作数)。 # vfmadd213pd 语义: ymmD = ymmS1*ymmD + mem(单次舍入, 真硬件 FMA)。 # 因此不再有 mul→tmp→add 两遍写法的中间数组与 1.5 倍内存流量; # 由于是单次舍入, Viper 的逐元素结果(及 CHECK)与 C/C++/Python 的 # "先乘后加两次舍入"可能差 1ulp(各实现内自洽, README §1 已注明)。 # # 命令行: app.exe [runs] [len] [iters] [mode] # runs : 默认 1; 0 = 仅热身(不输出 RESULT)。由 run_bench.py 传 10。 # len : 向量长度(4 的倍数, 默认 LEN) # iters: 每 run 内核趟数(默认 ITERS) # mode : 内核编号(见下方 KERNEL_* 常量), 默认 0 = FMA SIMD # 偶数 = SIMD(vipersimd._array) / 奇数 = 同操作的标量(原生)对照 # 「SIMD vs Native」用同一个 exe、只切换 mode, 除向量化外无其它差异。 # 命令行解析为自包含实现(GetCommandLineA 手工分词), 不依赖 sys/memhub。 # # 流程(与其它实现一致): # * 入口 OpenThread(GetCurrentThreadId()) 取真句柄, SetThreadAffinityMask 绑到 # 首个可用逻辑核并提线程优先级, 打印 AFFINITY ; # * 进程开头打印一次 CFG ; # * 分配 a/b/c/out 并用固定种子 LCG 填充 a/b/c(不计时); # * 2 轮满负荷热身(每轮 = ITERS 趟内核 + 升序读回 out 求校验和); # * 连续计时 runs 次, 每次 RESULT ; # * 全部结束后 CHECK (最后一次 run 的 out 升序累加)。 # 双计时只包住 ITERS 趟内核本身: # wall_us = QPC 墙钟微秒(含被抢占 / 被降频的时间, 对调度敏感); # cpu_us = QueryThreadCycleTime 本线程消耗的 CPU 周期数换算成微秒 # (只计真正执行过的周期, 对抢占 / 降频不敏感); # TSC 频率在入口用 QPC 标定(三次 30ms 忙等取最大), 并打印 TSC 。 # 不用 GetThreadTimes: 其记账粒度跟随调度时钟节拍(实测硬地板 15.625ms), # 几十毫秒的 run 误差 ±40%, 且 Win10 2004+ 无法用 timeBeginPeriod 改善。 # ============================================================ from stdint import * import vipersimd import math as vipermath import stdlib import stdio import t # 内核编号(命令行第 4 个参数): 只决定"跑哪份内核", 不影响计时/统计逻辑。 # 成对编号 = 同一操作的 SIMD 与标量(原生)对照, 供 SIMD 加速比测量: # 0 / 1 = FMA out = a*b + c —— 与 C/C++/Python 对齐的主内核 # 2 / 3 = ADD out = a + b # 4 / 5 = MUL out = a * b # 6 / 7 = SQRT out = sqrt(a) # 偶数 = SIMD(vipersimd._array), 奇数 = 标量(vipersimd.scalar__array)。 # 标量版用的是对应标量指令(vaddsd/vmulsd/vsqrtsd/vfmadd213sd), 与 SIMD 版 # 数学等价、逐位同结果, 因此两边的 CHECK 可直接对照。 define KERNEL_FMA_SIMD = 0 define KERNEL_FMA_SCALAR = 1 define KERNEL_ADD_SIMD = 2 define KERNEL_ADD_SCALAR = 3 define KERNEL_MUL_SIMD = 4 define KERNEL_MUL_SCALAR = 5 define KERNEL_SQRT_SIMD = 6 define KERNEL_SQRT_SCALAR = 7 define KERNEL_MAX = 7 # 合法 mode 上界 # 向量长度(4 的倍数, 满足 vipersimd 批量内核按 4 个 double 分批) # 负载约束: LEN × ITERS × 2 恒为 ≈2.147e9 flops/run(与 C/C++/Python 可比), # 只调 LEN 改"工作集": LEN=16384 -> 四份共 512 KiB, 稳居本机每核 1 MiB 私有 L2。 define LEN = 16384 define ITERS = 65536 # 每 run 内核趟数 define RUNS_MAX = 200 # runs 上限 # 伪随机 LCG 参数(minstd / Park-Miller), 固定种子 -> 可复现 define LCG_A = 48271.0 define LCG_M = 2147483647.0 define LCG_SEED = 12345.0 # Win32 常量 define THREAD_ALL_ACCESS = 0x1FFFFF # OpenThread 权限(足够绑亲和 + 查周期) define THREAD_PRIORITY_HIGHEST = 2 # SetThreadPriority 档位 # extern: Win32 高精度计时 (kernel32, 由 project.vpj 的 -lkernel32 链接) def QueryPerformanceCounter(lpPerformanceCount: t.CPtr) -> int | t.CExtern: pass def QueryPerformanceFrequency(lpFrequency: t.CPtr) -> int | t.CExtern: pass # extern: 原始命令行(整行, 含程序路径), 用于自解析第 1 个参数 def GetCommandLineA() -> CHARPTR | t.CExtern: pass # extern: 线程绑定 / 线程 CPU 周期(kernel32, 由 project.vpj 的 -lkernel32 链接) # 关键: 不要用 GetCurrentThread() —— 它返回伪句柄 (HANDLE)-2, 是个"负指针", # 在 Viper 的 t.CPtr 语义下未必能原样传给 C(实测 QueryThreadCycleTime 只会 # 写回一次调用开销 ~0.23us 的量级)。改用 GetCurrentThreadId() + OpenThread() # 拿一个真正的、纯数值的句柄, 全程用 t.CSizeT 表达, 彻底绕开指针/NULL 语义。 def GetCurrentThreadId() -> sizet | t.CExtern: pass def OpenThread(dwDesiredAccess: sizet, bInheritHandle: int, dwThreadId: sizet) -> sizet | t.CExtern: pass def SetThreadAffinityMask(hThread: sizet, dwThreadAffinityMask: sizet) -> sizet | t.CExtern: pass def SetThreadPriority(hThread: sizet, nPriority: int) -> int | t.CExtern: pass def QueryThreadCycleTime(hThread: sizet, CycleTime: t.CPtr) -> int | t.CExtern: pass # ------------------------------------------------------------ # 自包含命令行解析: 取第 idx 个整数参数(idx 从 0 起, 0 号是 runs)。 # 分词规则: 先跳过程序路径(容忍引号), 之后每个空白分隔的 token 记作一个参数; # token 里的十进制数字即该参数值, 缺失/非数字则返回 def_val。 # 命令行: app.exe [runs] [len] [iters] # ------------------------------------------------------------ def cmdline_int(idx: int, def_val: int64) -> int64: cmd: CHARPTR = GetCommandLineA() if cmd is None: return def_val i: sizet = 0 # 跳过前导空白 while cmd[i] == 32 or cmd[i] == 9: i += 1 # 跳过第 0 个 token(程序路径; 可能带引号) if cmd[i] == 34: i += 1 while cmd[i] != 0 and cmd[i] != 34: i += 1 if cmd[i] == 34: i += 1 else: while cmd[i] != 0 and cmd[i] != 32 and cmd[i] != 9: i += 1 # 逐个 token 往后找第 idx 个参数 for cur in range(idx + 1): while cmd[i] == 32 or cmd[i] == 9: i += 1 if cmd[i] == 0: return def_val # 读十进制整数(可空, 用 seen 区分 0 与"没有数字") val: int64 = 0 seen: int = 0 while cmd[i] >= 48 and cmd[i] <= 57: val = val * 10 + int64(cmd[i] - 48) i += 1 seen = 1 if cur == idx: if seen == 1: return val return def_val # 跳过本 token 剩余字符(保证每轮至少前进一个字符, 不会死循环) while cmd[i] != 0 and cmd[i] != 32 and cmd[i] != 9: i += 1 return def_val # ------------------------------------------------------------ # 填充 a/b/c: 逐元素依次消耗同一个 LCG 流(a[i], b[i], c[i]) # 迭代顺序与 C/C++/Python 完全一致, 保证输入逐位相同。 # ------------------------------------------------------------ def fill_abc(a: t.CPtr, b: t.CPtr, c: t.CPtr, n: sizet): da: double* = a db: double* = b dc: double* = c state: double = double(LCG_SEED) m: double = double(LCG_M) mul: double = double(LCG_A) for i in range(n): state = vipermath.fmod(state * mul, m) da[i] = state / m state = vipermath.fmod(state * mul, m) db[i] = state / m state = vipermath.fmod(state * mul, m) dc[i] = state / m # 校验和: 升序逐元素累加 out(与其它语言顺序一致; 读回保证写不被优化) def checksum_sum(out: t.CPtr, n: sizet) -> double: dout: double* = out s: double = double(0.0) for i in range(n): s += dout[i] return s # ------------------------------------------------------------ # 连续跑 iters 趟内核(热身与计时都走这里, 保证两边完全同一条路径)。 # mode 选内核(见顶部 KERNEL_*): 偶数 = SIMD, 奇数 = 同操作的标量(原生)对照。 # 分派用等值判断链而非函数指针: 每趟多一次分支, 相对"一趟内核"(微秒级)可忽略; # 且所有 mode 共用同一个调用点, 不会因代码布局差异污染 SIMD/原生对照。 # mode 已在 main 入口校验为 0..KERNEL_MAX, 所以最后一个 else 必然落在 # KERNEL_SQRT_SCALAR 上(与 else-if 链等价, 不是"兜底")。 # ------------------------------------------------------------ def run_iters(a: t.CPtr, b: t.CPtr, cbuf: t.CPtr, out: t.CPtr, n: sizet, iters: sizet, mode: int): for k in range(iters): if mode == int(KERNEL_FMA_SIMD): vipersimd.simd_fma_array(a, b, cbuf, out, n) elif mode == int(KERNEL_FMA_SCALAR): vipersimd.scalar_fma_array(a, b, cbuf, out, n) elif mode == int(KERNEL_ADD_SIMD): vipersimd.simd_add_array(a, b, out, n) elif mode == int(KERNEL_ADD_SCALAR): vipersimd.scalar_add_array(a, b, out, n) elif mode == int(KERNEL_MUL_SIMD): vipersimd.simd_mul_array(a, b, out, n) elif mode == int(KERNEL_MUL_SCALAR): vipersimd.scalar_mul_array(a, b, out, n) elif mode == int(KERNEL_SQRT_SIMD): vipersimd.simd_sqrt_array(a, out, n) else: vipersimd.scalar_sqrt_array(a, out, n) # 读取 8 字节有符号 64 位整数(有符号 64 位) # 既用于 QPC 写入的 LARGE_INTEGER.QuadPart, 也用于 QueryThreadCycleTime 写回的 ULONG64。 # 计时换算走纯 i64 整数路径: us = (dt * 1000000) / freq, 避免浮点表达式的低级错误 def qpc_ticks(p: t.CPtr) -> int64: v: INT64PTR = p return v[0] # ------------------------------------------------------------ # 打开当前线程的"真句柄"(失败返回 0)。不要用 GetCurrentThread(): 它返回 # 伪句柄 (HANDLE)-2(负指针), 在 Viper 的 t.CPtr 语义下传递不可靠。 # 进程退出时系统回收, 这里不显式 CloseHandle。 # ------------------------------------------------------------ def open_own_thread() -> sizet: return OpenThread(sizet(THREAD_ALL_ACCESS), int(0), GetCurrentThreadId()) # ------------------------------------------------------------ # 线程绑定: 绑到"第一个可用逻辑核", 返回 CPU 序号; 失败返回 -1。 # 用 mask = 1, 2, 4, 8 ... 逐个试: 第一个被系统接受的位就是进程亲和掩码里 # 编号最小的核 —— 四个实现会绑到同一个核, 横向对比才有意义。 # SetThreadAffinityMask 成功时返回"此前的掩码"(可能非 1), 失败返回 0。 # ------------------------------------------------------------ def bind_first_available_core(h: sizet) -> int: mask: sizet = sizet(1) for i in range(64): if SetThreadAffinityMask(h, mask) != sizet(0): return i mask = mask * sizet(2) return -1 # ------------------------------------------------------------ # 本线程累计消耗的 CPU 周期数(QueryThreadCycleTime 写回 cyc_buf, 8 字节)。 # 现代 x86 的 TSC 是 invariant 的, 与核心实际频率无关, 所以"周期数"就是 # "本线程真正在 CPU 上跑过的时间", 对抢占与降频都不敏感。 # ------------------------------------------------------------ def thread_cycles(h: sizet, cyc_buf: t.CPtr) -> int64: QueryThreadCycleTime(h, cyc_buf) return qpc_ticks(cyc_buf) # ------------------------------------------------------------ # 用 QPC 标定 TSC 频率(周期/秒): 忙等 30ms 数周期, 重复 3 次取最大。 # 为什么取最大: 标定窗口内本线程一旦被抢占, 消耗周期就少于墙钟流逝时间, # 算出 hz 偏小 -> 后续 cpu_us 系统性偏大(表现为"cpu_us 偶尔比 wall_us 还大" # 这种看着像 bug 的伪影)。取三次最大值 = 挑最没被打断的那一次。 # 必须在绑核之后调用, 否则跨核的 TSC 可能不同步。返回 0 = 标定失败。 # q0_buf/q1_buf 用于括住忙等窗口, cyc_buf 存周期数(都由调用方提供, 免重复分配)。 # ------------------------------------------------------------ def calibrate_tsc_hz(h: sizet, freq_i: int64, q0_buf: t.CPtr, q1_buf: t.CPtr, cyc_buf: t.CPtr) -> double: best: double = double(0.0) span_i: int64 = (freq_i * int64(30)) / int64(1000) # 30ms 的 QPC 计次 for trial in range(3): c0: int64 = thread_cycles(h, cyc_buf) QueryPerformanceCounter(q0_buf) dt: int64 = int64(0) while dt < span_i: QueryPerformanceCounter(q1_buf) dt = qpc_ticks(q1_buf) - qpc_ticks(q0_buf) c1: int64 = thread_cycles(h, cyc_buf) if dt > int64(0) and c1 > c0: hz: double = double(c1 - c0) * double(freq_i) / double(dt) if hz > best: best = hz return best def main() -> int: # 命令行: app.exe [runs] [len] [iters] [mode]; 省略则用顶部常量的默认值 runs_req: int64 = cmdline_int(0, int64(1)) n_req: int64 = cmdline_int(1, int64(LEN)) iters_req: int64 = cmdline_int(2, int64(ITERS)) mode_req: int64 = cmdline_int(3, int64(KERNEL_FMA_SIMD)) # 内核按 4-lane ymm 分批, LEN 必须是 4 的倍数; 参数非法直接报错, 不静默修正 if n_req < int64(4) or n_req % int64(4) != int64(0) or iters_req < int64(1): stdio.printf("bad args: len=%d(需 >=4 且为 4 的倍数) iters=%d\n", int(n_req), int(iters_req)) return 2 # mode 越界直接报错退出, 避免"悄悄跑成别的内核"污染 SIMD/原生对照 if mode_req < int64(0) or mode_req > int64(KERNEL_MAX): stdio.printf("bad args: mode=%d(只接受 0..7: FMA/ADD/MUL/SQRT 各一对 SIMD/原生)\n", int(mode_req)) return 2 if runs_req < int64(0): runs_req = int64(0) if runs_req > int64(RUNS_MAX): runs_req = int64(RUNS_MAX) n: sizet = sizet(n_req) iters: sizet = sizet(iters_req) mode: int = int(mode_req) # 0=SIMD / 1=Scalar(已校验) nb: sizet = n * sizet(8) nruns: sizet = sizet(runs_req) # 先拿当前线程的真句柄(失败就没法绑核/取周期, 直接报错退出, 不静默降级) h_thread: sizet = open_own_thread() if h_thread == sizet(0): stdio.printf("OpenThread(GetCurrentThreadId()) failed\n") return 1 # 线程绑定(必做) + 提优先级: 与 C/C++/Python 同口径, 并压低背景进程抢占的尖峰 core: int = bind_first_available_core(h_thread) SetThreadPriority(h_thread, int(THREAD_PRIORITY_HIGHEST)) # 四块缓冲区 malloc(对齐由 stdlib 保证 16B 以上); 单趟内核不再需要 tmp a: t.CPtr = stdlib.malloc(nb) b: t.CPtr = stdlib.malloc(nb) c: t.CPtr = stdlib.malloc(nb) out: t.CPtr = stdlib.malloc(nb) if not a or not b or not c or not out: stdio.printf("malloc failed\n") return 1 # 进程开头打印一次配置(其它实现同一协议: CFG + AFFINITY ) stdio.printf("CFG %d %d\n", int(n), int(iters)) stdio.printf("MODE %d\n", mode) stdio.printf("AFFINITY %d\n", core) # 输入生成一次(不计时) fill_abc(a, b, c, n) # QPC 频率/计次缓冲 + CPU 周期缓冲(均 == 8 字节) freq_buf: t.CPtr = stdlib.malloc(sizet(8)) t0_buf: t.CPtr = stdlib.malloc(sizet(8)) t1_buf: t.CPtr = stdlib.malloc(sizet(8)) cyc_buf: t.CPtr = stdlib.malloc(sizet(8)) if not freq_buf or not t0_buf or not t1_buf or not cyc_buf: stdio.printf("malloc failed\n") return 1 QueryPerformanceFrequency(freq_buf) freq_i: int64 = qpc_ticks(freq_buf) if freq_i <= int64(0): freq_i = int64(10000000) # 兜底: 假设 10MHz(与实测一致) # TSC 频率标定(必须在绑核之后): 把 QueryThreadCycleTime 的周期数换算成微秒 tsc_hz: double = calibrate_tsc_hz(h_thread, freq_i, t0_buf, t1_buf, cyc_buf) stdio.printf("TSC %.4e\n", tsc_hz) # 运算量用纯整数表示(展示/换算时才转 double), 绕开浮点表达式低级错误。 # 每元素浮点运算数: FMA = 2(乘+加), ADD / MUL / SQRT = 1。 ops_per_elem: int64 = int64(1) if mode == int(KERNEL_FMA_SIMD) or mode == int(KERNEL_FMA_SCALAR): ops_per_elem = int64(2) total_i: int64 = int64(n) * int64(iters) * ops_per_elem last_chk: double = double(0.0) # 2 轮满负荷热身(每轮 = ITERS 趟内核 + 读回 out 求校验和) for w in range(sizet(2)): run_iters(a, b, c, out, n, iters, mode) last_chk = checksum_sum(out, n) # 连续计时 nruns 次; 计时只包住 ITERS 趟内核本身 # 双计时: 墙钟 QPC + 线程 CPU(QueryThreadCycleTime), 同时包住同一段内核 for r in range(nruns): cy0: int64 = thread_cycles(h_thread, cyc_buf) QueryPerformanceCounter(t0_buf) run_iters(a, b, c, out, n, iters, mode) QueryPerformanceCounter(t1_buf) cy1: int64 = thread_cycles(h_thread, cyc_buf) # i64 整数换算: us = (dt * 1e6) / freq, 杜绝浮点表达式的读回歧义 dt_i: int64 = qpc_ticks(t1_buf) - qpc_ticks(t0_buf) us_i: int64 = (dt_i * int64(1000000)) / freq_i us: double = double(us_i) # 周期 -> 微秒; 标定失败(tsc_hz==0)时给 0, 表现为一眼可见的异常 cpu_us: double = double(0.0) if tsc_hz > double(0.0): cpu_us = double(cy1 - cy0) * double(1000000.0) / tsc_hz flops_i: int64 = (total_i * int64(1000000)) / us_i flops: double = double(flops_i) stdio.printf("RESULT %.3f %.3f %.0f\n", us, cpu_us, flops) # 计时区外读回 out: 使本轮写入可观察, 阻止跨 run 优化 last_chk = checksum_sum(out, n) stdio.printf("CHECK %.17g\n", last_chk) return 0