简介:LLCbench(Low-Level Characterization Benchmarks)是一套面向 Linux 平台的底层性能表征测试工具集,集成了 MPBench、CacheBench 与 BLASBench 三种测试方法,本包聚焦其中的 CacheBench,用于测量处理器各级缓存的读写带宽与访问延迟,适合系统调优、体系结构研究及高性能计算方向的学习者动手验证不同工作集规模下的缓存表现。压缩包共 81 个文件、约 83KB,以 C 源码、Makefile 编译脚本、sys.def 平台配置文件、gnuplot 绘图脚本与 ps 结果图、html 与 tex 文档为主,配合 README、CHANGES 及 doc 目录,可完成从编译、运行到出图的完整流程;def 配置覆盖 linux、alpha、ppc、solaris、ia64 等多种平台与 MPI 环境,scripts、misc 目录另附辅助脚本。已有 873 人学习。借助内置的 cachebench.c 与绘图脚本,读者可复现缓存性能曲线、对照 results 中的示例输出,理解缓存层次与数据规模之间的关系,也可作为课程实验与性能调优的入门素材。
1. llcbench 是什么:三套基准打一个包,测的是缓存和算力的底
拿到 llcbench.tar.gz 的人,第一反应往往是「这不就是个跑分脚本吗」,解压完看到 cachebench、blasbench、mpbench 三个目录,又有点拿不准。它确实不是单一工具,而是把 Linux 上做处理器性能测试最常用的三块基准打进了一个包:cachebench 量缓存和内存的带宽、延迟、随机访问,blasbench 量浮点算力,mpbench 量多进程通信。做服务器选型、虚拟化资源核算、或者调完编译参数后做回归对比的从业者,会反复用到它。它不会给你一个总分,测试规模、绑定策略、基线怎么定,全得你自己拿主意,数字才有意义。
2. 编译这套老代码:目录结构、-fcommon 与 BLAS/MPI 依赖补齐
llcbench 的代码年头不短,直接make大概率会在某个环节卡住。先把目录摸清楚,再分开编译,比一把梭好定位得多。
2.1 解包后的目录结构:三个独立基准加一层共用代码
解包动作很朴素,但进去之后先别急着敲 make:
# 解包并进入源码树 tar xzf llcbench.tar.gz cd llcbench # 看顶层结构,确认三个基准目录都在 ls -F典型的顶层内容会包含cachebench/、blasbench/、mpbench/三个基准目录,外加一个放公共计时和工具函数的源码目录、一个顶层 Makefile 和 README。三个子目录各自有独立的 Makefile,这意味着你可以只编自己需要的那个,不用为 mpbench 去折腾 MPI 环境。
我一般先在 cachebench 里试水,因为它依赖最少,只用到 libc 和数学库:
# 只编 cachebench,先验证工具链能过 cd cachebench make linuxlinux是 cachebench 的 Makefile 里预置的目标,展开后基本就是拿 cc 加-O2把几个源文件编成一个可执行文件,再链上-lm。如果这一步就报错,问题基本锁定在编译器版本或头文件上,跟 BLAS、MPI 无关。编完先跑一下最短的测试,确认二进制不是残废的:
# 用很小的内存规模快速冒烟,看能不能正常出数 ./cachebench -b -m 16能打出几行带宽数字就说明编译链路通了。这一步别省,后面 blasbench 和 mpbench 出问题时,你能立刻判断是「基准本身坏了」还是「依赖没接对」。
2.2 GCC 10 以上编译失败:重复符号与 -fcommon
这套代码最经典的翻车点,是在较新的 GCC 上编不过,报一串multiple definition of ...。原因不玄学:老式 C 代码习惯在头文件里直接定义全局变量,而不是只放extern声明。GCC 10 起默认-fno-common,多个编译单元各自展开一份定义,链接器就直接掀桌子。
解决办法有两个,我更倾向不动 Makefile 的那个:
# 用 CC 变量把 -fcommon 塞进编译命令,避免改源码树 cd cachebench make clean make linux CC="gcc -fcommon"如果 Makefile 里把CC写死成cc并且不接受覆盖,那就退一步直接改编译选项:
# 把 CFLAGS 整行替换掉,保留 -O2 并加上 -fcommon sed -i 's/^CFLAGS.*/CFLAGS = -O2 -fcommon/' Makefile make clean && make linux这里有两个参数值得说清楚。-O2不建议降到-O0,因为 cachebench 测的是内存子系统,编译器优化等级会明显影响循环展开和访存指令的生成,-O0下测出来的带宽会系统性偏低。-fcommon只是让重复定义合并成一份,不改变运行期行为,加它不会有精度副作用。
blasbench 和 mpbench 大概率会遇到同一类问题,处理方式一样。三个目录都编过一遍之后,再回头检查有没有 warning 里藏着「隐式声明」这种雷——老代码里函数没原型的情况不少,在 64 位平台上可能表现为指针被截断,跑起来直接段错误。
2.3 BLAS 与 MPI 依赖:blasbench、mpbench 的接线方式
blasbench 要链外部 BLAS 库,Makefile 里通常默认写的是-lblas。系统里如果只装了参考实现,能编过,但测出来的数字会难看得让你怀疑机器坏了。我的做法是明确指定库:
# 链 OpenBLAS,显式给库名和线程库 cd ../blasbench make clean make BLASLIB="-lopenblas -lpthread"如果你的环境装的是别的实现,把-lopenblas换成对应的库名即可。这里的关键认知是:blasbench 测出来的峰值算力,很大程度上是「你链的这个 BLAS 库在这个 CPU 上的表现」,而不是 CPU 的理论峰值。换了库,数字会变,对比时必须跟库绑定记录。
mpbench 的依赖是 MPI 编译器包装器:
# 用 mpicc 编,确保头文件和链接路径都由 MPI 环境提供 cd ../mpbench make clean make MPICC=mpicc编完不要直接./mpbench,它是 MPI 程序,得用启动器拉起:
# 两个进程做点对点测试,先确认能跑通 mpirun -np 2 ./mpbench单机跑 mpbench 有个前提:共享内存传输通道要能用。容器环境里这一层经常被裁掉,表现是进程起来了但卡在初始化,或者退回 TCP 回环,延迟数字大一个数量级。这个坑在后面的排查章节里单独说。
提示:三个基准建议在同一个 shell 会话里连续编完,把每一条完整编译命令记进一个
build.sh。这套代码的编译选项对结果影响很大,半年后想复现同一组数字,靠记忆基本靠不住。
3. cachebench 三项测试:带宽、延迟、随机访问的跑法与规模设定
cachebench 是这套包里用得最多的部分。它的思路不复杂:给一块缓冲区,用不同的访问模式去踩它,计时算吞吐或单次访问开销。难点不在跑,而在「工作集设多大」——这决定了你测到的是 L1、L2、L3 还是内存。
3.1 带宽测试:-b 背后读、写、拷贝三条链路
-b打开的是带宽模式,内部会跑顺序读、顺序写和拷贝三类操作。读是遍历缓冲区累加,写是按步长填值,拷贝等价于一次 memcpy 风格的搬运。三类操作的瓶颈并不一样:读受预取器和内存控制器影响大,写会牵涉写分配策略,拷贝同时吃读写两条通路,所以同一个工作集下拷贝带宽通常低于纯读。
最小跑法:
# 512MB 工作集,远超多数平台的 LLC,测的是内存侧带宽 ./cachebench -b -m 512想要看出缓存层次的拐点,得扫一组规模,而不是跑一个点:
# 从 4KB 扫到 1GB,覆盖 L1 到内存 for m in 4 16 64 256 1024; do echo "=== working set ${m}MB ===" ./cachebench -b -m $m done把每个规模下的读带宽画成曲线,你会看到明显的台阶:工作集小于 L1 时带宽最高,跨过 L2、L3 逐级下降,最后在内存带宽附近走平。台阶的位置就是你机器缓存容量的实测值,比查规格表靠谱——规格表写的是理论容量,实际可用容量还要扣掉关联度和替换策略带来的损耗。
多核聚合带宽不能靠单个进程测。cachebench 的并行选项有限,常见做法是起多个实例,用taskset各绑一个物理核:
# 四个实例分别绑到 0-3 号核,观察聚合带宽是否线性 for c in 0 1 2 3; do taskset -c $c ./cachebench -b -m 512 > bw_core_$c.log & done wait跑完把四个日志里的带宽值加起来,和单核数字对比。如果四核聚合只有单核的两倍出头,通常说明已经撞到内存通道带宽上限,或者绑到了同一物理核的两个超线程上。
3.2 延迟与随机访问:-l 和 -r 的规模怎么定
-l测的是单次访存的延迟,实现方式是指针追逐:把缓冲区切成固定大小的槽位,每个槽位存下一个槽位的地址,串成一个环,然后沿着环一路跳。因为下一次访问的地址依赖上一次读出来的值,CPU 没法提前预取,也没法并行发射,测出来的就是真实访存延迟。
# 延迟测试,工作集分别落在 L2 内和内存侧 ./cachebench -l -m 1 ./cachebench -l -m 512结果的量级感要建立起来:L1 命中大概在 1 纳秒量级,L2 几个纳秒,L3 十几到几十纳秒,跨到内存通常在大几十到一百多纳秒。如果你在 512MB 工作集下测出个位数纳秒,别高兴,大概率是指针环没串对,或者被编译器优化掉了一部分访问。
-r是随机访问模式,步长伪随机,专门用来打 TLB 和硬件预取器的脸。它和-l的区别在于:延迟测试每次只关心一跳的耗时,随机测试关心的是在一段随机序列下整体能跑出多少有效吞吐。工作集超过 TLB 覆盖范围之后,随机访问的性能会断崖式下跌,这个跌幅本身就是很有价值的指标——它告诉你业务里那种指针满天飞的代码,在这颗 CPU 上会付多大代价。
# 随机访问,重点看工作集跨过 TLB 覆盖范围后的跌幅 ./cachebench -r -m 64 ./cachebench -r -m 10243.3 参数表与输出字段:把 MB/s 读成缓存层次结论
cachebench 的常用参数不多,但每个都影响结论,整理成表更清楚:
| 参数 | 含义 | 我是怎么设的 |
|---|---|---|
-b | 带宽测试 | 扫规模时必用,关注读、写、拷贝三条线 |
-l | 延迟测试 | 配小工作集测缓存,配大工作集测内存 |
-r | 随机访问 | 看跨 TLB 边界后的性能衰减 |
-a | 全部测试 | 首次摸底时用,耗时最长 |
-m | 工作集大小,单位 MB | 按 2 的幂次扫,至少覆盖 L1 到内存四档 |
输出字段的核心是三列:规模、耗时、带宽或延迟。耗时单位看具体版本,有的打秒,有的打微秒,读之前先确认。带宽列一般是 MB/s,注意是十进制 MB 还是 MiB,做跨工具对比时这个差异能造成几个百分点偏差,别小看。
真正要形成结论,得把「规模—带宽」曲线和 CPU 规格对上。比如你的平台标称 L3 是 32MB,曲线在 16MB 到 32MB 之间开始掉档,说明可用容量大概在这个区间;如果曲线在 4MB 就掉了,那要么是关联度设计导致的有效容量缩水,要么是测试时绑到了共享 L3 的核上,被邻居干扰了。这类判断没法靠单次测试得出,得配合绑核和多次重复。
注意:cachebench 单次运行时间随工作集线性增长,1GB 规模下可能要跑几分钟。批量扫描时先用小规模验证脚本逻辑,确认没问题再放开规模,别一上来就挂一晚上。
4. blasbench 与 mpbench:浮点算力和多进程扩展怎么量
cachebench 管的是数据搬运,blasbench 管的是算,mpbench 管的是多个进程之间怎么协作。三块拼起来,才是一台机器在 HPC 或高密度服务场景下的完整画像。
4.1 blasbench 接哪个 BLAS 库,直接决定你测的是谁
blasbench 的做法是对一组不同维度的方阵做双精度矩阵乘,用2*N^3除以耗时算 GFLOPS。矩阵乘是典型的计算密集型负载,理论上能榨干 FMA 单元,所以它常被当成浮点峰值的近似。
跑法很直接:
# 交互式跑一遍,观察不同矩阵规模下的 GFLOPS 曲线 cd blasbench ./blasbench结果会随矩阵维度变化:小矩阵受调用开销和缓存局部性拖累,中大矩阵逐渐爬上峰值平台区,再大则因为超出缓存开始回落。你要找的是那条平台区的高度,以及平台区对应的维度范围——后者决定了你的业务矩阵规模落在什么区间时能吃满算力。
选型上最容易踩的坑是没意识到「测的是库不是 CPU」。同一颗 CPU 上,参考 BLAS、单线程 OpenBLAS、多线程 OpenBLAS、MKL 跑出来的峰值可以差出数倍。所以记录结果时至少要写清三件事:库名、线程数、CPU 频率策略。少一项,这组数字半年后就没法用了。
# 显式控制线程数,避免 OpenBLAS 自己探测出一堆线程把结果搅浑 OPENBLAS_NUM_THREADS=8 ./blasbench > blas_8t.log OPENBLAS_NUM_THREADS=1 ./blasbench > blas_1t.log单线程和多线程两份数字都有用:单线程那份反映单核的指令级并行能力,多线程那份反映扩展效率。两者一除,就是这颗 CPU 在这个矩阵规模下的并行加速比,比看厂商宣传页实在。
4.2 mpbench 的 ping-pong、广播与 allreduce
mpbench 覆盖的是 MPI 通信里最常用的几类原语。点对点 ping-pong 测的是两个进程之间一发一收的往返延迟,以及大消息下的单向带宽;广播和 allreduce 测的是一对多的分发效率;alltoall 测的是全交换,最容易在大规模下暴露网络或共享内存通道的瓶颈。
先跑默认配置确认环境通:
# 两进程冒烟,确认 MPI 栈能起来 mpirun -np 2 ./mpbench再按消息大小扫一遍,才能看出延迟和带宽的分界:
# 从小到大扫消息尺寸,小消息看延迟,大消息看带宽 for sz in 8 1024 65536 1048576; do echo "=== msg size ${sz} ===" mpirun -np 2 ./mpbench 2>&1 | tail -n 20 done小消息区间里,曲线几乎是平的,因为耗时被固定开销主导,这时候比较的是各家 MPI 实现的软件路径长短。消息尺寸跨过某个阈值之后,耗时开始随尺寸线性上涨,斜率倒数就是有效带宽。这个拐点位置很有参考价值:业务里消息如果普遍小于拐点,优化方向应该是减少通信次数、合并消息;大于拐点,才轮到考虑带宽和通道。
单机多进程的情况下,进程间其实走的是共享内存通道,测出来的延迟应该明显低于走网络的跨机场景。如果单机跑出来的延迟和跨机差不多,先怀疑通道没走对,而不是机器性能不行。
4.3 规模、重复次数与绑定:让结果可复现的三件事
blasbench 和 mpbench 的结果波动比 cachebench 大,因为涉及多线程和多进程调度。想让数字站得住,至少做三件事。
第一,绑核。用taskset或numactl把进程钉死在确定的核上,避免调度器把进程在 NUMA 节点之间搬来搬去。跨节点访存的代价在延迟测试里能差出几十纳秒。
# 绑到 0-7 号核,并优先在 0 号 NUMA 节点分配内存 numactl --cpunodebind=0 --membind=0 ./blasbench第二,重复。单次结果不收,至少跑三次取中位数。第一次运行往往还带着页表冷启动和频率爬升的影响,直接采信容易高估或低估。
第三,记录环境。CPU governor 是 performance 还是 powersave、有没有开超线程、内核版本、MPI 实现和版本,这些都要跟结果写在一起。同一台机器换了个 governor,带宽数字差 15% 完全不奇怪。
# 跑之前把环境快照存下来,跟结果放同一个目录 { uname -a cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor mpirun --version | head -n 2 } > env_snapshot.txt这份快照看起来啰嗦,但等到你要回答「上个月那组数字为什么对不上」的时候,它就是唯一的后悔药。
5. 避坑与排查:五条跑 llcbench 踩出来的记录
这套工具本身不复杂,复杂的是环境。下面五条是我在不同机器上反复撞见的,按现象、原因、解决拆开写,碰上了可以直接对号入座。
5.1 带宽测出来只有标称的一半
现象:机器标称内存带宽很高,cachebench 大工作集下跑出来的读带宽却只有一半左右,换工作集大小也没改善。
原因通常有两个。一是绑核绑错了,进程落在某个 NUMA 节点的核上,内存却从另一个节点分配,跨节点访存直接把有效带宽砍半。二是通道没插满,很多机器只插了一半内存槽位,标称带宽是满通道的理论值。
解决:先用numactl --hardware看清楚节点和内存分布,再用--membind把内存绑到同一节点。然后dmidecode或带外管理界面确认实际插槽使用情况。这两步做完再跑,数字一般能回到合理区间。
5.2 同一台机器两次跑,数字差 20%
现象:什么都没改,隔十分钟再跑一遍,带宽或算力差了百分之十几到二十。
原因:频率策略和邻居干扰。governor 在 powersave 下会根据负载动态调频,第一次跑的时候频率还没爬满;虚拟化环境里邻居虚机的负载也会通过共享缓存和内存通道传导过来。
解决:跑之前把 governor 切到 performance 并等频率稳定,物理机上关掉或记录 turbo 状态,虚拟机上接受这个波动并在报告里写明测试时段。对比时用中位数而不是单次值,差异超过 5% 就追加重复次数。
5.3 编译通过,运行时段错误
现象:make 全绿,一执行就 segmentation fault,连第一行输出都没有。
原因:老代码里的隐式函数声明在 64 位平台上把指针截断成 int,或者 Makefile 里的-march指向了目标机不支持的指令集,编译机能跑、目标机一执行就炸。
解决:先看编译日志里的 implicit declaration 警告,逐个补上正确的头文件或加原型声明。再把-march=native换成明确的架构等级重编,确认是不是指令集不匹配。定位手段就一个:gdb ./cachebench跑起来看崩在哪一行。
5.4 两台机器对比,结论完全反了
现象:A 机单核带宽高于 B 机,但多核聚合带宽反过来,或者延迟测试和带宽测试给出互相矛盾的排序。
原因:对比维度没对齐。工作集大小、绑核方式、线程数、governor 状态、内存通道配置,任何一项不同都会污染结论。最常见的是拿 A 机的单通道配置去比 B 机的双通道配置,然后得出「A 的 CPU 不行」这种错误判断。
解决:把测试脚本连同参数一起固化,两台机器跑同一份脚本,输出同一份环境快照,逐项核对差异。对不上的项要么消除,要么在结论里明确标注为混杂因素。
5.5 mpbench 在容器里跑不出结果
现象:mpirun -np 2 ./mpbench卡住不动,或者跑出来的点对点延迟和跨机网络一个量级。
原因:容器默认裁掉了共享内存相关的资源,MPI 的进程间通道退化成了 TCP 回环,甚至直接初始化失败。
解决:容器启动时加上--ipc=host放开共享内存,必要时显式指定走共享内存通道。跑之前先用一个两进程的 hello 程序验证通道,确认延迟在微秒以下再上 mpbench,否则测出来的数字没有参考价值。
6. 把数字变成结论:交叉验证与基线固化
单跑一套基准,得到的只是孤立的数。要把它们变成能写进选型报告或调优结论的东西,还得做两件事:交叉验证和基线固化。
交叉验证的意思是,同一类指标至少用两条独立路径测一遍,看结论是否一致。带宽可以用 cachebench 的-b结果去对另一套内存带宽测试工具的输出,两者量级应该接近,偏差超过 20% 就得查原因——常见的是工作集定义不同,一方测的是 LLC 内,一方已经打到内存。浮点算力可以拿 blasbench 的峰值去对编译器自动向量化后的简单循环,如果后者反而更高,说明 BLAS 库没吃到这台机器的指令集,得回头检查库的编译选项。延迟这块,cachebench 的指针追逐结果可以跟性能计数器里统计的缓存未命中代价对照,两条路给出的数量级一致,才敢往下写结论。
基线固化更琐碎,但价值最高。我习惯把整套流程写成一个脚本,脚本里做四件事:切 governor、绑核绑内存、跑三组基准、把结果和环境快照归档到带时间戳的目录。
#!/usr/bin/env bash # 一次完整的基线采集,输出到带时间戳的目录 set -euo pipefail OUT="baseline_$(date +%Y%m%d_%H%M%S)" mkdir -p "$OUT" # 固定频率策略,减少动态调频带来的抖动 echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null # 环境快照先落盘,后面所有数字都跟它绑定 uname -a > "$OUT/env.txt" cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor >> "$OUT/env.txt" # 三个基准各跑三次,中位数后面手工取 for i in 1 2 3; do numactl --cpunodebind=0 --membind=0 ./cachebench/cachebench -b -m 512 > "$OUT/cache_$i.log" OPENBLAS_NUM_THREADS=8 ./blasbench/blasbench > "$OUT/blas_$i.log" mpirun -np 2 ./mpbench/mpbench > "$OUT/mpi_$i.log" 2>&1 done脚本本身没什么技巧,关键是坚持跑。我吃过一次亏:调完编译参数觉得性能肯定涨了,凭印象写了个「提升约 15%」进报告,后来被人要求复现,发现当时没存环境快照,governor 状态、绑核方式全对不上,那 15% 根本没法证明。从那以后我每次动性能相关的参数,都强制走一遍这个脚本,哪怕只是改了一个编译选项。数字可以不好看,但必须能复现。
希望帮到你。
本文还有配套的精品资源,点击获取