Files
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

280 lines
11 KiB
C
Raw Permalink 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.
/*
* bench_c.c - 手写 C 基准实现 (进程内多次 run, 单进程输出多行)
*
* 内核(与 App/main.vp / 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 次浮点运算/元素,
* 即每次 run 总 flops = LEN*ITERS*2 ≈ 2.147e9。
*
* 命令行: bench_c.exe [runs]
* runs: 默认 1; 0 = 仅做热身(不输出 RESULT); 建议 10(由 run_bench.py 传入)。
*
* 流程:
* 0) 入口先 SetThreadAffinityMask 绑到首个可用逻辑核(消除跨核迁移 / 大小核
* 异构 / 缓存失效抖动), 并以 AFFINITY <core> 行回报(-1 = 绑定失败, 不致命);
* 1) 进程开头打印一次 CFG <len> <iters>;
* 2) 分配 a/b/c/out 并用固定种子 LCG 填充 a/b/c(不计时);
* 3) 2 轮"满负荷热身"(每轮 = ITERS 趟内核 + 升序读回 out 求和), 之后再计时;
* 4) 连续计时 runs 次, 每次输出一行:
* RESULT <wall_us> <cpu_us> <flops>
* wall_us = QPC 墙钟微秒(含被抢占 / 被降频的时间, 对调度敏感);
* cpu_us = QueryThreadCycleTime 线程 CPU 微秒(只计本线程真正执行过的
* 周期, 对抢占 / 降频不敏感; TSC 频率在入口用 QPC 标定);
* flops = (LEN*ITERS*2)/秒, 按墙钟秒换算(与其它实现同口径);
* 计时只包住 ITERS 趟内核本身(不含分配/填充/热身/读回校验);
* 5) 全部结束后输出 CHECK <sum>(用最后一次 run 留下的 out 升序累加)。
*
* 防优化器删除 / 别名约束:
* - 所有指针一律加 __restrict(与 bench_cpp.cpp 同一拼写, clang/GCC/MSVC 都认):
* out 与 a/b/c 互不重叠, 关掉"保守别名分析"这个变量, 让 C 的自动向量化
* 行为与 C++ 收敛(旧版 C 写 C99 的 restrict、C++ 写 __restrict, 两份源码
* 拼写不一致, 容易把差异误判成语言差异);
* - 编译用 -O2 -mavx2 -mfma -ffp-contract=fast: 允许 FMA 收缩,
* out[i]=a[i]*b[i]+c[i] 生成单趟 vfmadd(单次舍入), 与 Viper 的
* simd_fma_array 同口径(不再强制"先乘后加"两次舍入);
* - 每轮热身与每次 run 之后都升序读回 out 求校验和 -> 计时区内的每一条
* out 写都"可观察", 不可能被整趟折叠/删除; noinline 防止内核被内联后
* 与循环展开/标签冲突并杜绝跨趟优化。
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* QueryThreadCycleTime 是 Vista+ API: 显式抬到 0x0601 让 mingw 头文件声明它 */
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#endif
#include <windows.h>
/* 负载约束: LEN × ITERS × 2 恒为 ≈2.147e9 flops/run(与其它实现可比),
* 只调 LEN 来改"工作集"大小。
* LEN=16384 -> a/b/c/out 四份共 512 KiB, 稳居本机(AMD Ryzen AI 7 H 350)
* 每核 1 MiB 私有 L2 之内: 访存不再打到共享 L3 / DRAM, 也就不会被其它进程
* 抢带宽 —— 这正是之前 C++ 出现"零星高时延尖峰"的主因。
* 再往下压到 L1(需 LEN<=1024)会让每趟的内核调用/派发开销占比飙升,
* 对 Python+numpy 尤其致命(实测每趟固定开销约 0.4us)。 */
#define LEN 16384 /* 元素数(2^14, 4 的倍数, 满足 ymm 4-lane 分批) */
#define ITERS 65536 /* 每 run 的内核趟数(令 LEN*ITERS*2 仍 ≈2.147e9) */
#define CFG_RUNS_MAX 200 /* runs 上限(防止误传超大值) */
#define LCG_A 48271.0
#define LCG_M 2147483647.0
#define LCG_SEED 12345.0
#if defined(_MSC_VER)
#define NOINLINE __declspec(noinline)
#else
#define NOINLINE __attribute__((noinline))
#endif
/* ------------------------------------------------------------
* 线程绑定: 把当前线程绑到"第一个可用逻辑核", 返回该 CPU 序号; 失败返回 -1。
* 从 1<<0 逐个试, 第一个被系统接受的位就是进程亲和掩码里编号最小的核 ——
* 同一台机器上四个实现会绑到同一个核, 横向对比才有意义。
* ------------------------------------------------------------ */
static int bind_first_available_core(void) {
int i;
const int nbits = (int)(sizeof(DWORD_PTR) * 8);
for (i = 0; i < nbits; ++i) {
if (SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)1 << i) != 0) {
return i;
}
}
return -1;
}
/* ------------------------------------------------------------
* 线程 CPU 时间: QueryThreadCycleTime 返回本线程真正消耗过的 CPU 周期数
* (现代 x86 的 TSC 是 invariant 的, 与核心实际频率无关)。
*
* 为什么不用 GetThreadTimes: 它的记账粒度跟随调度时钟节拍, 本机实测硬地板
* 15.625ms —— 一个几十毫秒的 run 只能被记成 2~3 个节拍, 误差 ±40%,
* 连"CPU 时间 > 墙钟时间"这种物理上不可能的结果都会出现。
* 且 timeBeginPeriod(1) 在 Win10 2004+ 已无法改善该粒度(实测确认无效)。
* QueryThreadCycleTime 的粒度是周期级, 彻底解决这个问题。
* ------------------------------------------------------------ */
static long long query_thread_cycles(void) {
ULONG64 cycles = 0;
QueryThreadCycleTime(GetCurrentThread(), &cycles);
return (long long)cycles;
}
/* 用 QPC 标定 TSC 频率(周期/秒): 忙等 30ms 数周期, 重复 3 次取最大。返回 0 = 失败。
*
* 为什么要取最大而不是平均: 标定窗口内本线程一旦被抢占, 消耗的周期数就少于
* 墙钟经过的时间, 算出 hz 偏小 -> 后续 cpu_us 系统性偏大(实测约 +0.13%,
* 会表现为"cpu_us 偶尔比 wall_us 还大"这种看着像 bug 的伪影)。取三次里的
* 最大值 = 挑"最没被打断"的那次, 把该偏置压掉。
* 必须在 SetThreadAffinityMask 之后调用, 否则跨核的 TSC 可能不同步。 */
static double calibrate_tsc_hz(LARGE_INTEGER freq) {
double best = 0.0;
int t;
for (t = 0; t < 3; ++t) {
LARGE_INTEGER a, b;
long long c0, c1;
double span = (double)freq.QuadPart * 0.030; /* 30ms 对应的 QPC 计次 */
double dt, hz;
c0 = query_thread_cycles();
QueryPerformanceCounter(&a);
do {
QueryPerformanceCounter(&b);
} while ((double)(b.QuadPart - a.QuadPart) < span);
c1 = query_thread_cycles();
dt = (double)(b.QuadPart - a.QuadPart);
if (dt <= 0.0 || c1 <= c0) {
continue;
}
hz = (double)(c1 - c0) * (double)freq.QuadPart / dt;
if (hz > best) {
best = hz;
}
}
return best;
}
/* 填充 a/b/c: 逐元素依次消耗同一个 LCG 流(a[i], b[i], c[i]) */
static void fill_abc(double *__restrict a,
double *__restrict b,
double *__restrict c,
long n) {
double state = LCG_SEED;
const double m = LCG_M;
const double mul = LCG_A;
long i;
for (i = 0; i < n; ++i) {
state = fmod(state * mul, m);
a[i] = state / m;
state = fmod(state * mul, m);
b[i] = state / m;
state = fmod(state * mul, m);
c[i] = state / m;
}
}
/* 内核: out[i] = a[i]*b[i] + c[i] (元素间独立, -O2 + AVX2 + FMA 自动向量化单趟融合乘加)
* __restrict: 四个指针互不别名, 编译器无需为"可能重叠"生成保守标量路径。 */
static NOINLINE void kernel_fma(double *__restrict out,
const double *__restrict a,
const double *__restrict b,
const double *__restrict c,
long n) {
long i;
for (i = 0; i < n; ++i) {
out[i] = a[i] * b[i] + c[i];
}
}
/* 校验和: 升序逐元素累加 out(读回 out -> 保证内核的写未被删除) */
static double checksum_sum(const double *__restrict out, long n) {
double s = 0.0;
long i;
for (i = 0; i < n; ++i) {
s += out[i];
}
return s;
}
int main(int argc, char **argv) {
/* 命令行: bench_c.exe [runs] [len] [iters]
* 省略则用上面 #define 的默认值; run_bench.py 做多尺寸扫描时会显式传入。 */
long n = LEN;
long iters = ITERS;
size_t nb;
double total_flops;
/* 四个缓冲各自独立, 指针全部 __restrict: 与内核形参同一 no-alias 承诺 */
double *__restrict a, *__restrict b, *__restrict c, *__restrict out;
LARGE_INTEGER freq, t0, t1;
double last_chk = 0.0;
double tsc_hz;
int runs = 1;
int r;
int core;
long k;
if (argc > 1) {
runs = atoi(argv[1]);
}
if (argc > 2) {
n = atol(argv[2]);
}
if (argc > 3) {
iters = atol(argv[3]);
}
if (runs < 0) runs = 0;
if (runs > CFG_RUNS_MAX) runs = CFG_RUNS_MAX;
/* 内核按 4-lane ymm 分批, LEN 必须是 4 的倍数; 参数非法直接报错, 不静默修正 */
if (n < 4 || n % 4 != 0 || iters < 1) {
fprintf(stderr, "bad args: len=%ld(需 >=4 且为 4 的倍数) iters=%ld\n", n, iters);
return 2;
}
nb = (size_t)n * sizeof(double);
total_flops = (double)n * (double)iters * 2.0;
/* 线程绑定(必做) + 提优先级: 消除跨核迁移, 并压低背景进程抢占造成的零星尖峰 */
core = bind_first_available_core();
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
printf("CFG %ld %ld\n", n, iters);
printf("AFFINITY %d\n", core);
fflush(stdout);
a = (double *)malloc(nb);
b = (double *)malloc(nb);
c = (double *)malloc(nb);
out = (double *)malloc(nb);
if (!a || !b || !c || !out) {
fprintf(stderr, "malloc failed\n");
return 1;
}
/* 输入生成一次(不计时) */
fill_abc(a, b, c, n);
QueryPerformanceFrequency(&freq);
/* TSC 频率标定(必须在绑核之后): 把 QueryThreadCycleTime 的周期数换算成微秒 */
tsc_hz = calibrate_tsc_hz(freq);
printf("TSC %.4e\n", tsc_hz);
fflush(stdout);
/* 2 轮满负荷热身(每轮 = ITERS 趟内核 + 读回 out 求校验和) */
for (r = 0; r < 2; ++r) {
for (k = 0; k < iters; ++k) {
kernel_fma(out, a, b, c, n);
}
last_chk = checksum_sum(out, n);
}
/* 连续计时 runs 次; 计时只包住 ITERS 趟内核
* 双计时: 墙钟 QPC + 线程 CPU(QueryThreadCycleTime), 同时包住同一段内核 */
for (r = 0; r < runs; ++r) {
double us, cpu_us, flops;
long long cy0, cy1;
cy0 = query_thread_cycles();
QueryPerformanceCounter(&t0);
for (k = 0; k < iters; ++k) {
kernel_fma(out, a, b, c, n);
}
QueryPerformanceCounter(&t1);
cy1 = query_thread_cycles();
us = ((double)(t1.QuadPart - t0.QuadPart) * 1000000.0) /
(double)freq.QuadPart;
/* 周期 -> 微秒; 标定失败(tsc_hz==0)时给 0, 表现为一眼可见的异常 */
cpu_us = (tsc_hz > 0.0) ? (double)(cy1 - cy0) * 1000000.0 / tsc_hz : 0.0;
flops = total_flops / (us * 1e-6);
printf("RESULT %.3f %.3f %.0f\n", us, cpu_us, flops);
fflush(stdout);
/* 计时区外读回 out: 使本轮写入可观察, 阻止跨 run 的写折叠 */
last_chk = checksum_sum(out, n);
}
printf("CHECK %.17g\n", last_chk);
fflush(stdout);
free(a);
free(b);
free(c);
free(out);
return 0;
}