本机 C++ microbench
对应 个人性能测试记录。Linux x86-64、C++20,无第三方 benchmark 库。源码入口在 microbench.cpp 的 main(),每组实验是一个独立函数。
编译与运行
cd /home/martins3/data/vn/docs/benchmark/microbench
./run.sh
脚本重新编译,创建独立的 results/日期时间.随机后缀/,保存环境、原始 CSV、进度日志和运行后的 load average。完整采样每个配置 5 次;最大同时使用 32 个逻辑 CPU,主要内存分配峰值约 512 MiB。不会清理系统缓存、更改 governor、关闭安全防护或者重启机器。二进制以 .out 结尾。
本机同时有 Nix 和系统工具链,因此 Makefile 使用 /usr/bin/g++ -B/usr/bin/,保证系统编译器、链接器和运行库配套;否则 PATH 中的 Nix 链接器可能注入另一版本的 glibc。编译参数为 -O3 -g -std=c++20 -march=native。如需其他工具链,可同时覆盖 CXX 和 CXXFLAGS。-march=native 的产物只应在兼容 CPU 上运行。
make smoke
./microbench.out --group memory --samples 5 > memory.csv
./microbench.out --group compute --cpu 16 --samples 5 > compute-e-core.csv
./microbench.out --group scaling --samples 7 > scaling.csv
./microbench.out --group context --samples 5 > context.csv
sudo ./microbench.out --group syscall --samples 5 > syscall-pmu.csv
--quick 减少大部分循环次数,用于功能检查,不作为正式结果。大工作集指针链仍至少访问两遍。--cpu 指定串行实验和通信发起者;scaling 始终使用程序启动时允许的整个 CPU 集合。可以通过 taskset -c 限制这个集合。程序自动按 sysfs 的 socket/core/sibling 信息分组,不把连续编号误当成独立物理核。smt_cores 指具备 SMT 的核,single_thread_cores 指没有 SMT sibling 的核;仅在这台已核实的 13900K 上分别解释为 P/E 核。
stdout 只有 CSV,stderr 是进度、PMU 可用性和最终 checksum。perf_event_paranoid 阻止内核态计数时仍执行耗时测试,并明确提示 PMU 不可用,不填伪造的零。
实验与计量边界
| 分组 / 函数 | 测试方法 | 输出和限制 |
|---|---|---|
compute / compute() |
64 位无符号整数乘加,一条依赖链与四条独立链 | ns/update;一次 update 是一次乘加,包含循环开销,不是单条指令延迟。禁止 SIMD 合并,体现标量指令级并行 |
branch / branch_loop() |
同一组 64 KiB 的 0/1 数据,各占一半;排序与随机打乱 | ns/branch;内联汇编固定条件跳转,避免被优化为 cmov。差值包含预测和流水线等影响,不是直接测得的单次预测失败惩罚 |
memory / memory()、chase() |
4 KiB 到 256 MiB,每 64 B 一节点,固定种子构造遍历所有节点的随机环 | ns/load;真正串行依赖的 pointer chasing,无法通过并发访存隐藏延迟。使用 MADV_NOHUGEPAGE,因此大工作集也包含 TLB miss/page walk,不能称为纯 DRAM 延迟 |
memory / memory() |
32 KiB、1 MiB、16 MiB、256 MiB 的 libc memcpy |
GB/s 是有效载荷字节数/时间,十进制 GB;实际访存流量可能包含读、写和 RFO。CSV 工作集大小是单个缓冲区,源+目标占两倍 |
syscall / syscalls() |
循环基线、raw getpid、libc clock_gettime、raw clock_gettime、向 /dev/null 写 1 B |
ns/call。raw 路径用 syscall(),避免把 vDSO 当成陷入内核;libc 时钟路径在本机使用 vDSO |
syscall_cpu / Usage |
用 getrusage(RUSAGE_THREAD) 观察批量操作的用户态/内核态 CPU 时间 |
user_ns/call、system_ns/call;有记账粒度和测量边界开销,用于解释 us/sys,不适合推断极短操作的精确分账 |
syscall_pmu / Counters |
当前线程的 perf cycles/instructions 事件组,包含内核态,排除 hypervisor | cycles/call、instructions/call;包含用户态循环、libc 包装及极少的计时/PMU 控制开销。不是 syscall 内核函数单独的指令数,也不是指令种类分布。若发生 multiplexing 则报错,不盲目缩放 |
scaling / scaling() |
多种核放置,分别执行依赖链和四条独立链;每个配置的总工作量固定 | Mupdates/s;预热和创建线程不计时,起止 barrier 计时。配置顺序逐轮轮换。包含不同核的频率差异、同步开销和尾部慢核影响 |
coherence / coherence() |
两线程在同一 atomic token 上 acquire-load/release-store 交替,自旋等待,使用 pause |
ns/roundtrip;一次完整往返,不是单向延迟。包含轮询和线程执行时间;SMT sibling 共享资源,不能统一解释为核间互连延迟 |
sharing / coherence() |
两线程分别对两个 atomic counter 做 relaxed fetch_add;同一 cache line 与两个独立 line 对比 |
ns/increment 是总墙钟时间除以两个线程的总操作数,即吞吐量的倒数,不是单次 RMW 的响应时间。计数结束后验证结果 |
context / context() |
两条 pipe 传 1 B,pthread 与 fork 进程,比较同一逻辑 CPU、SMT sibling、不同物理核 | ns/roundtrip;含四次读写 syscall、pipe、阻塞、唤醒、调度及数据传递,不是纯 switch_to() 或内核线程切换开销 |
context_switches / context() |
发起线程的 ru_nvcsw 和 ru_nivcsw 差值 |
每轮自愿/非自愿切换次数;只统计发起者,不能当作双方总数。跨核两端可并行执行 |
防止测到错误东西
- 主线程、每个工作线程和子进程都显式绑定 CPU,亲和性设置失败则终止。拓扑从绑定前的允许集合读取。
- 每项先预热,再计时;内存分配、随机排列、首次缺页和线程创建在计时外。内存预触摸不保证后续绝无回收或中断干扰。
- 使用
steady_clock批量计时,除以操作次数;没有将 TSC tick 直接称为 CPU cycle,也没有从墙钟和标称频率推导 cycles。 - 计算结果保留到 checksum;编译器屏障防止消除乘加和 memcpy。可用
objdump -d -C microbench.out查看chain()、independent()、branch_loop()、chase()。 - CSV 保存每次样本,不仅保存最好的一轮;正文给出中位数及必要的 min/max。少量批次的最大值不是单操作 p99。
- 没有锁定睿频、隔离 CPU 或暂停其他本机任务。运行中的负载、温度、频率和共享 LLC 会影响数据,结论只适用于记录中的这次实验。
- 缓存行按本机核实的 64 B 编写。此套件不是任意架构的自动调参工具。
CSV 字段
group,case,cpus,working_set_bytes,sample,operations,elapsed_ns,unit,value
cpus 用 + 连接 CPU 编号,避免与 CSV 分隔符冲突;sample 从 0 开始。operations 在 memcpy 行表示累计载荷字节,在其他行表示该项定义的迭代、乘加、往返或原子增量次数。对 (group, case, cpus, working_set_bytes, unit) 分组后取 value 的中位数即可复现正文统计。所有时间结果越小越好,GB/s 和 Mupdates/s 越大越好。
与原始笔记的关系
core-to-core-latency 通过固定两线程的 CPU 来测缓存一致性通信。本实现使用 load/store token 往返而非相同 CAS 协议,数值不能直接和它的表格混用。
Tsuna 的 context-switch 文章 是原笔记的参考阅读;这里明确把 pipe 往返称为调度和通信的组合测试,不把往返除二当作纯上下文切换耗时。
内核完整构建、VM host/guest 对照、真实编辑器输入到显示的延迟、安全防护开关的 reboot A/B,以及指令类别分布,都需要进一步的专项实验。当前 microbench 不替代它们。
STREAM 式物理内存带宽
stream.cpp 是独立的 AVX2/FMA 测试,不是官方 STREAM 提交版本。默认每数组
512 MiB,共三个数组,比较 4 KiB 页和 THP;每配置预热一批,再测七批,
每批连续扫描四遍。输出到 stdout 的是逐批 CSV,stderr 是中位数和 min/max。
make stream
./stream.out > stream.csv 2> stream.log
./stream.out --pages thp,4k --mib 1024 --samples 9 --passes 8 --threads 1,2,4,8
./stream.out --pages thp --cpus 16,17,18,19,20,21,22,23 --threads 1,2,4,8
./stream.out --pages hugetlb --mib 512 --threads 1,2,4,8
cpu_order()从当前允许的 CPU 集合及 sysfs 拓扑取每物理核一个线程,最后才添加 SMT siblings。--cpus可显式指定顺序;--threads取该列表前 N 个 CPU。 默认线程数扫 1/2/4/8/16/24/32 以及可用 CPU 总数。CSV 的cpus保留真实放置。Mapping用MADV_NOHUGEPAGE强制 4 KiB 页,THP 用 2 MiB 对齐映射、MADV_HUGEPAGE,并在系统头文件支持时请求MADV_COLLAPSE。huge_kib_before/after来自本次三个数组的/proc/self/smaps,表示计时前后 实际大页覆盖的 KiB 数。THP 请求可能只部分成功,必须检查覆盖率。--pages hugetlb显式请求 2 MiBMAP_HUGETLB,需要足够的大页池及权限; 程序不会修改系统大页池,申请失败直接报错,不会静默降级。默认不运行该模式。run_op()在计时外并行初始化、核实页映射和创建工作线程;工作线程跨样本复用。 起止 barrier 的 completion function 记录墙钟,计时包含同步唤醒与慢线程拖尾。 分块边界为 128 B 的倍数,支持 3、7、24 等线程数,不越界、不共享边界缓存行。 数组在同一页模式内复用,改变线程数不会迁移已有物理页;跨 NUMA 机器需另外控制 内存放置。本次物理机只有一个 NUMA node。read_avx()使用四个独立 AVX 累加器;普通写和 NT 写显式使用不同存储指令,triad/triad_nt使用 FMA。每次扫描包含编译器屏障,存储循环末尾执行 fence, 但 fence 不会把所有普通脏缓存行强制写回 DRAM。- 每配置结束后在计时外检查三个数组的每个元素及读操作 checksum,校验成功才输出。 小数组可用于校验程序,但会测到缓存,不能纳入 DRAM 峰值。正式结果应独立复测, 并用更大数组排查缓存影响。
GB/s = payload_bytes / elapsed_ns,均为十进制。计数口径如下:
| 操作 | 每个 double 的计数字节 | 典型 DRAM 流量模型 |
|---|---|---|
| read | 8 | 8 |
| write | 8 | 16(含 RFO) |
| copy | 16 | 24(含 RFO) |
| triad | 24 | 32(含 RFO) |
| write_nt | 8 | 8 |
| copy_nt | 16 | 16 |
| triad_nt | 24 | 24 |
这些模型不能代替内存控制器硬件计数器:缓存命中、写回时机和处理器优化都可能
改变实际流量。copy 的有效读写带宽是复制文件常用的“源数据字节数/时间”的两倍。
测试不锁频、不暂停其他程序、不清缓存;输出的最大样本是观察到的最好一批,
不是机器在所有条件下的硬件极限。
页模式依据:Linux THP 文档、 HugeTLB 文档; 计数口径依据:STREAM FAQ。
双通道 interleaving:跨页物理地址测试
interleave.cpp 用两个 IMC 的硬件计数器测地址的通道归属。默认分配 256 MiB
普通页内存池,从中随机选择物理地址 A 和 A XOR (1 << bit),每个地址位最多
32 对,逐位观察是否换通道。bit 12 及以上使用真实的跨物理页配对,不把虚拟地址
连续当作物理地址连续,也不再仅拟合单页内的规律。
make interleave
./interleave.out --self-test
sudo ./interleave.out
sudo ./interleave.out --mib 1024 --pairs 64
sudo ./interleave.out --access write --cpu 2
默认读取测试,每地址 5 个样本,每样本 20,000 次
CLFLUSH → MFENCE → load → LFENCE;写模式使用整行 NT store + MFENCE,供交叉验证。
程序只在终端输出汇总,不生成 CSV、日志或结果目录。默认运行通常需要几分钟;
增加 --pairs 会延长采样时间,增加 --mib 主要扩大寻找物理配对的内存池。
也支持 --cpu、--samples 和 --iterations。
每次负载前后测背景流量,用两个 IMC 的背景扣除后差分判定归属;弱信号和明显
波动被拒绝。每地址至少 2/3 样本同意且没有有效反例才接受。每位至少 4 对、至少
2/3 配对有效,且换通道效果一致,才判定该位的效果。找不到物理配对时显示
not covered,结果矛盾或样本不足显示 inconclusive,不会当作“不参与 hash”。
输出包含分配的内存池大小、实际采样页数、物理地址范围及各位换通道统计。 范围是稀疏采样的首尾,不代表穷举其间全部内存;逐位结果也不是完整 hash 公式。 最低换通道位只有在所有更低位已确认不换通道时才报告。
需要 Linux x86-64、Intel Alder/Raptor Lake 裸机、两个
uncore_imc_free_running PMU,以及系统级 perf、pagemap PFN 和 mlock 权限,通常用 sudo。
分配采用 MADV_NOHUGEPAGE,预触摸并 mlock,测量前后核实 PFN。该测试识别 IMC,
本机每个 IMC 对应一个 DDR4 通道;不能直接用于区分 DDR5 子通道或 rank/bank。
计数语义见 Intel 的
ADL/RPL PMU 事件定义。
为什么编码规则需要有 xor hash ?
下面把 0–127 当作 512B 块编号:第 n 块对应物理地址 [n×512, (n+1)×512−1]。假设 C=0;若 C=1,把所有 0、1 对调。
按我们假设的公式:
channel = PA[9] ⊕ PA[10] ⊕ PA[13] ⊕ PA[19]
这 128 块共 64KiB,PA[19] 始终为 0,换成块编号就是:
channel(n) = n[0] ⊕ n[1] ⊕ n[4]
块编号 对应通道,从左到右 ━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0–15 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 ───────── ──────────────────────────────────── 16–31 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 ───────── ──────────────────────────────────── 32–47 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 ───────── ──────────────────────────────────── 48–63 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 ───────── ──────────────────────────────────── 64–79 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 ───────── ──────────────────────────────────── 80–95 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 ───────── ──────────────────────────────────── 96–111 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 ───────── ──────────────────────────────────── 112–127 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1
每 16 块,即每 8KiB,PA[13] 翻转,因此下一组的通道分布整体反相。
这么设计的主要目的,是让不同访问步长也能利用两个通道。 Intel 对 Channel Hashing 的说明也是:结合多个地址位,使内存访问更均匀地分布到各通道。Intel Hot Chips 演讲中的 “Uncore IPC Features” (https://old.hotchips.org/wp-content/uploads/hc_archives/hc24/HotChips24.Proceedings-revised-12-09-07.pdf)
举一个按该公式推导的例子,程序每隔 8KiB 读一个位置:
物理地址: 0 8KiB 16KiB 24KiB 32KiB 40KiB 只用 PA[9]: 0 0 0 0 0 0 使用 XOR: 0 1 0 1 0 1
只用 bit 9 时,这些地址全部挤在通道 0,通道 1 的带宽用不上。加入 bit 13 后,这种步长就能分散到两个通道。加入其他位,则可 以改善其他尺度的访问模式。
XOR 的硬件实现也很简单:四个位的异或可以用三个二输入 XOR 门组合完成,不需要 CPU 执行计算。
不过,它不能保证所有步长都均匀。例如按这个模型,以 1MiB 步长访问各块起点,这四个位都不变,仍会落在同一通道。它改善的是多 种常见访问模式;至于本机为何恰好选择这四个位和 512B 粒度,不能仅凭这次测量确定。
本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。