Skip to the content.

本机 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 差值 每轮自愿/非自愿切换次数;只统计发起者,不能当作双方总数。跨核两端可并行执行

防止测到错误东西

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

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 将按侵权追究法律责任,其它情况随意。