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

238 lines
9.2 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_cpp.cpp - 手写 C++ 基准实现 (进程内多次 run, 单进程输出多行)
*
* 与 bench_c.c 完全同口径:
* 内核 out[i] = a[i]*b[i] + c[i], LEN = 16384, 每次 run = ITERS = 65536 趟,
* 总 flops ≈ 2.147e9; 命令行第 1 参数 = runs(默认 1; 0 = 仅热身)。
*
* - 容器 std::vector; 内核经裸指针 + __restrict 传入, 便于自动向量化。
* - 线程绑定: 入口 SetThreadAffinityMask 绑到首个可用逻辑核, 以 AFFINITY <core>
* 行回报(-1 = 绑定失败)。与 bench_c.c / main.vp / bench_py.py 绑同一个核。
* - 双计时: 每次 run 同时采集 QPC 墙钟微秒 与 QueryThreadCycleTime 线程 CPU
* 微秒, 输出 RESULT <wall_us> <cpu_us> <flops>(flops 按墙钟秒换算)。
* - 随机输入与 CHECK 口径与 C/Viper/Python 完全一致(固定种子 minstd LCG)。
* - 编译口径与 bench_c.c 相同: -O2 -mavx2 -mfma -ffp-contract=fast,
* 允许 FMA 收缩成单趟 vfmadd(单次舍入), 与 Viper 的 simd_fma_array 对齐。
* - 防删: 每轮热身/每次 run 后升序读回 out 求 CHECK; noinline; __restrict。
*/
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <vector>
/* QueryThreadCycleTime 是 Vista+ API: 显式抬到 0x0601 让 mingw 头文件声明它 */
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#endif
#include <windows.h>
namespace {
/* 负载约束: N_ELEM × N_ITERS × 2 恒为 ≈2.147e9 flops/run(与其它实现可比),
* 只调 N_ELEM 来改"工作集"大小。
* N_ELEM=16384 -> a/b/c/out 四份共 512 KiB, 稳居本机每核 1 MiB 私有 L2 之内:
* 访存不再打到共享 L3 / DRAM, 也就不会被其它进程抢带宽 —— 这正是之前 C++
* 出现"零星高时延尖峰"的主因(与 std::vector/向量化无关, 计时区内核逐字相同)。 */
constexpr long N_ELEM = 16384;
constexpr long N_ITERS = 65536;
constexpr int RUNS_MAX = 200;
constexpr double LCG_A = 48271.0;
constexpr double LCG_M = 2147483647.0;
constexpr double LCG_SEED = 12345.0;
/* ------------------------------------------------------------
* 线程绑定: 绑到"第一个可用逻辑核", 返回 CPU 序号; 失败返回 -1。
* 从 1<<0 逐个试, 第一个被接受的位就是进程亲和掩码里编号最小的核 ——
* 四个实现绑到同一个核, 横向对比才有意义。
* ------------------------------------------------------------ */
int bind_first_available_core() {
const int nbits = static_cast<int>(sizeof(DWORD_PTR) * 8);
for (int i = 0; i < nbits; ++i) {
if (SetThreadAffinityMask(GetCurrentThread(),
static_cast<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+ 已无法改善该粒度(实测确认无效)。
* ------------------------------------------------------------ */
long long query_thread_cycles() {
ULONG64 cycles = 0;
QueryThreadCycleTime(GetCurrentThread(), &cycles);
return static_cast<long long>(cycles);
}
/* 用 QPC 标定 TSC 频率(周期/秒): 忙等 30ms 数周期, 重复 3 次取最大。0.0 = 失败。
*
* 为什么取最大: 标定窗口内本线程一旦被抢占, 消耗周期就少于墙钟流逝时间,
* 算出 hz 偏小 -> 后续 cpu_us 系统性偏大(实测约 +0.13%, 表现为"cpu_us 偶尔
* 比 wall_us 还大"这种看着像 bug 的伪影)。取三次最大值 = 挑最没被打断的一次。
* 必须在 SetThreadAffinityMask 之后调用, 否则跨核的 TSC 可能不同步。 */
double calibrate_tsc_hz(const LARGE_INTEGER &freq) {
const double span = static_cast<double>(freq.QuadPart) * 0.030; /* 30ms 的 QPC 计次 */
double best = 0.0;
for (int t = 0; t < 3; ++t) {
LARGE_INTEGER a{}, b{};
const long long c0 = query_thread_cycles();
QueryPerformanceCounter(&a);
do {
QueryPerformanceCounter(&b);
} while (static_cast<double>(b.QuadPart - a.QuadPart) < span);
const long long c1 = query_thread_cycles();
const double dt = static_cast<double>(b.QuadPart - a.QuadPart);
if (dt <= 0.0 || c1 <= c0) {
continue;
}
const double hz =
static_cast<double>(c1 - c0) * static_cast<double>(freq.QuadPart) / dt;
if (hz > best) {
best = hz;
}
}
return best;
}
#if defined(_MSC_VER)
#define NOINLINE __declspec(noinline)
#else
#define NOINLINE __attribute__((noinline))
#endif
/* 填充 a/b/c: 逐元素依次消耗同一个 LCG 流(a[i], b[i], c[i]) */
void fill_abc(double *a, double *b, double *c, long n) {
double state = LCG_SEED;
const double m = LCG_M;
const double mul = LCG_A;
for (long i = 0; i < n; ++i) {
state = std::fmod(state * mul, m);
a[i] = state / m;
state = std::fmod(state * mul, m);
b[i] = state / m;
state = std::fmod(state * mul, m);
c[i] = state / m;
}
}
/* 内核: out[i] = a[i]*b[i] + c[i] (元素间独立, -O2 + AVX2 + FMA 自动向量化单趟融合乘加) */
static NOINLINE void kernel_fma(double *__restrict out,
const double *__restrict a,
const double *__restrict b,
const double *__restrict c,
long n) {
for (long i = 0; i < n; ++i) {
out[i] = a[i] * b[i] + c[i];
}
}
/* 校验和: 升序逐元素累加 out(读回 out -> 保证内核的写未被删除) */
double checksum_sum(const double *__restrict out, long n) {
double s = 0.0;
for (long i = 0; i < n; ++i) {
s += out[i];
}
return s;
}
} // namespace
int main(int argc, char **argv) {
/* 命令行: bench_cpp.exe [runs] [len] [iters]
* 省略则用上面 constexpr 的默认值; run_bench.py 多尺寸扫描时会显式传入。 */
long n = N_ELEM;
long iters = N_ITERS;
int runs = 1;
if (argc > 1) {
runs = std::atoi(argv[1]);
}
if (argc > 2) {
n = std::atol(argv[2]);
}
if (argc > 3) {
iters = std::atol(argv[3]);
}
if (runs < 0) runs = 0;
if (runs > RUNS_MAX) runs = RUNS_MAX;
/* 内核按 4-lane ymm 分批, LEN 必须是 4 的倍数; 参数非法直接报错, 不静默修正 */
if (n < 4 || n % 4 != 0 || iters < 1) {
std::fprintf(stderr, "bad args: len=%ld(需 >=4 且为 4 的倍数) iters=%ld\n", n, iters);
return 2;
}
const double total_flops = static_cast<double>(n) * static_cast<double>(iters) * 2.0;
/* 线程绑定(必做) + 提优先级: 消除跨核迁移, 并压低背景进程抢占造成的零星尖峰 */
const int core = bind_first_available_core();
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
std::printf("CFG %ld %ld\n", n, iters);
std::printf("AFFINITY %d\n", core);
std::fflush(stdout);
std::vector<double> a(n), b(n), c(n), out(n);
double *pa = a.data();
double *pb = b.data();
double *pc = c.data();
double *pout = out.data();
LARGE_INTEGER freq, t0, t1;
double last_chk = 0.0;
/* 输入生成一次(不计时) */
fill_abc(pa, pb, pc, n);
QueryPerformanceFrequency(&freq);
/* TSC 频率标定(必须在绑核之后): 把 QueryThreadCycleTime 的周期数换算成微秒 */
const double tsc_hz = calibrate_tsc_hz(freq);
std::printf("TSC %.4e\n", tsc_hz);
std::fflush(stdout);
/* 2 轮满负荷热身(每轮 = ITERS 趟内核 + 读回 out 求校验和) */
for (int r = 0; r < 2; ++r) {
for (long k = 0; k < iters; ++k) {
kernel_fma(pout, pa, pb, pc, n);
}
last_chk = checksum_sum(pout, n);
}
/* 连续计时 runs 次; 计时只包住 ITERS 趟内核
* 双计时: 墙钟 QPC + 线程 CPU(QueryThreadCycleTime), 同时包住同一段内核 */
for (int r = 0; r < runs; ++r) {
const long long cy0 = query_thread_cycles();
QueryPerformanceCounter(&t0);
for (long k = 0; k < iters; ++k) {
kernel_fma(pout, pa, pb, pc, n);
}
QueryPerformanceCounter(&t1);
const long long cy1 = query_thread_cycles();
const double us =
(static_cast<double>(t1.QuadPart - t0.QuadPart) * 1000000.0) /
static_cast<double>(freq.QuadPart);
/* 周期 -> 微秒; 标定失败(tsc_hz==0)时给 0, 表现为一眼可见的异常 */
const double cpu_us = (tsc_hz > 0.0)
? static_cast<double>(cy1 - cy0) * 1000000.0 / tsc_hz : 0.0;
const double flops = total_flops / (us * 1e-6);
std::printf("RESULT %.3f %.3f %.0f\n", us, cpu_us, flops);
std::fflush(stdout);
/* 计时区外读回 out: 阻止跨 run 的写折叠 */
last_chk = checksum_sum(pout, n);
}
std::printf("CHECK %.17g\n", last_chk);
std::fflush(stdout);
return 0;
}