SPEC CPU2017 这套东西,第一次接触的人多半是被"安装"两个字骗进来的。下载一个 ISO、跑一个 install.sh,听起来跟装个数据库差不多,真正上手才发现从挂载到出分之间隔着一整条链路:编译器版本、cfg 配置文件的字段、runspec 的参数组合、跑完之后的 ratio 怎么算,任何一环出问题都只会给你一句看不懂的报错。这篇文章把我自己在 x86 服务器上反复装、反复跑 speccpu2017 的过程完整梳理一遍,重点放在安装与使用这两个动作上——ISO 怎么挂、install.sh 会问你什么、cfg 文件哪些字段必须改、runspec 从冒烟测试到正式出分怎么敲、以及编译和运行阶段最容易翻车的几个位置。全文假设你已经通过正规渠道拿到了 ISO 并接受了它的许可条款,后面的操作都在拿到文件之后展开。
1. 先把 SPEC CPU2017 这套东西的脾气摸清楚
1.1 它不是一个软件,是四个套件加一套打分规则
很多人以为 SPEC CPU2017 是"一个跑分程序",装上以后敲一条命令就出分。实际它是四个互相独立的套件加一套打分规则:SPECspeed 2017 Integer(10 个基准程序)、SPECspeed 2017 Floating Point(13 个)、SPECrate 2017 Integer(10 个)、SPECrate 2017 Floating Point(13 个)。同一个算法在 speed 和 rate 里会出现两次,编号和源码都不一样,比如整数里的600.perlbench_s和500.perlbench_r是两个独立的目录、两套独立的源码和数据集,后缀_s/_r就是它们的身份标识。
这意味着你在 cfg 里针对600.perlbench_s做的一切定制,对500.perlbench_r完全不起作用,要改就得分别改。刚开始我不理解这个设计,觉得重复劳动,后来才明白:speed 和 rate 的负载特征不同,需要不同的编译开关,把它们拆成两套源码反而让配置更清晰。安装完之后benchspec/CPU/下面会摊开几十个目录,每个目录里都带自己完整的src、data、spec三件套,这就是它动辄占几个 G 的原因。
还有一个容易忽略的点:SPEC CPU2017 的源码和数据集是受许可约束的产品,不是开源项目。你能在自己机器上编译、运行、看结果,但把编译产物或者数据集二次分发是另一回事。商用场景下要留意这一点,学术和研究用途相对宽松,但具体边界还是要看拿到 ISO 时接受的那份条款。我在这里只讨论技术操作,不涉及获取途径。
1.2 SPECspeed 与 SPECrate:测延迟还是测吞吐
这两个词最容易混。SPECspeed 测的是"一件事做多快",本质上更接近单任务延迟:一次只算一个基准程序,看它多长时间跑完,再把参考机耗时除以实测耗时得到 ratio,最后对一批 ratio 取几何平均,就是SPECspeed2017_int_base这种总分。规则允许的情况下,对本身支持 OpenMP 的基准程序可以用--threads给多线程。
SPECrate 测的是"单位时间能做多少件事",也就是吞吐。它的做法是在同一台机器上同时起 N 个独立的进程副本,每个副本跑同一份负载,最后用副本数 × 参考耗时 / 实测耗时得到 ratio。所以 rate 的分数和--copies直接挂钩:8 副本和 64 副本测出来的数字不是一回事,跨平台比分数时第一个要确认的就是对方用了多少副本。
这个区别直接决定了你怎么配机器。跑 rate 的时候,多副本会同时抢 LLC 和内存带宽,超线程在这种场景下往往是负收益,把 SMT 关掉、一个物理核跑一个副本通常更好看;跑 speed 的时候,少数能吃多线程的基准程序反而希望 SMT 打开,让线程能塞进超线程里。我见过不少人拿 speed 的调优手法去跑 rate,结果分数比预期低一截,还以为是机器有问题,其实就是副本数没算对。
1.3 base 和 peak 差在哪,什么时候只需要跑 base
base 是"有约束的公平赛道":同一套件内所有基准程序必须用一致的优化选项,不能针对某一个程序单独下猛药,允许的优化级别也有上限。peak 是"放开手脚":你可以给602.gcc_s单独加一组对它有利的开关,给627.cam4_s换另一组,只要能通过正确性校验就行。
对绝大多数使用场景——比较两代 CPU、验证编译器版本升级带来的收益、评估采购方案——只跑 base 就够了。peak 的调优工作量是指数级的,而且 peak 分数高度依赖你花了多少时间堆开关,可比性反而不如 base。我自己的做法是:内部评估只跑--tune=base --size=ref --iterations=3,只在需要对外提交正式结果时才动 peak。
2. 装之前的两笔账:机器够不够、编译器选哪个
2.1 内存与磁盘的经验估算
SPEC CPU2017 官方给出的内存底线不高,但这只是"能跑起来"的底线,不是"跑得舒服"的底线。实际跑--size=ref时,621.wrf_s、627.cam4_s、628.pop2_s这几个浮点程序单副本的常驻内存就能到 1 GB 以上,跑 rate 时按副本数线性叠加,64 副本的场景下几百 G 内存眨眼就没了。我的经验值是这样的:
| 使用场景 | 内存建议 | 说明 |
|---|---|---|
| 只跑 test 尺寸冒烟测试 | 8 GB | 够用,但别指望跑 ref |
单套件 ref,--copies=1 | 16 GB | intspeed 这类基本不会 OOM |
单套件 ref,--copies=8~16 | 64 GB | rate 场景的常见配置 |
| 全套件 ref,高副本数 | 128 GB 起 | 建议按物理核数 × 2 GB 估算 |
磁盘这块,ISO 本身几个 G,安装解压后在 3~4 GB 量级,但真正吃空间的是编译产物和运行日志。每个基准程序的build/目录里会同时留着源码副本、目标文件和可执行文件,四个套件全编一遍再跑几轮,轻轻松松吃掉二三十个 G。我一般直接留 100 GB,省得跑到一半因为磁盘满导致specmake报一堆莫名其妙的错。磁盘写满这个坑特别阴——报错信息通常出现在编译阶段,看起来像是源码问题,实际是没地方写.o文件。
2.2 GCC 版本别追新,选 9 到 12 最省心
编译器版本是第二个要先定下来的事。SPEC 提供的示例 cfg 通常按发行版默认的 GCC 写好,而发行版默认的 GCC 往往比 SPEC 官方验证过的版本更新。新版本 GCC 做了两件对跑分很不友好的事:一是把一些原来只是警告的诊断提升为错误,二是默认行为变了(比如更强的最严格别名规则、默认打开某些新优化),导致某些基准程序直接编不过或者算错。
我自己的选择范围是GCC 9 到 GCC 12。GCC 8 及以下在部分浮点程序上优化不足,GCC 13 以后新引入的诊断偶尔会和 SPEC 自带的specmake打架。如果你的机器上只有很新的 GCC,有几个办法:装一个老版本的 GCC 单独放一个路径,在 cfg 里把CC/CXX/FC写成绝对路径指过去;或者在 cfg 里加-Wno-error之类的开关压掉新诊断。前者的确定性更高,我更推荐。
Clang 也能跑,但要注意FC那一项——SPEC CPU2017 里浮点套件大量使用 Fortran,Clang 本身不带 Fortran 前端,你需要额外配flang或者让FC指向 GCC 的gfortran。混用编译器的组合不是不行,但一旦出现数值校验失败,排查起来会比纯 GCC 麻烦好几倍。首次上手建议全套 GCC,等链路跑通了再考虑换组合。
2.3 依赖包清单与检查命令
装之前把这些包确认一遍,能省掉后面一半的报错:
gcc、g++、gfortran:三个都要,gfortran最容易被漏掉libgfortran运行库(有些发行版拆成单独的包)make、binutils:给specmake兜底perl:runspec本身依赖 Perl,SPEC 包里自带specperl,但系统里有一个 Perl 会更稳numactl:跑 NUMA 机器绑定用cpupower:调频率策略用
一条命令快速自查:
for c in gcc g++ gfortran make perl numactl; do printf "%-10s " "$c"; command -v $c || echo "MISSING" donecommand -v比which可靠,在容器或者精简系统里which可能根本没装。如果在容器里跑,还要确认两件事:一是 CPU 亲和性没有被 cgroup 限死,二是--copies不能超过容器实际能用的核数,否则 SPEC 会直接判定运行无效。
3. 从 ISO 到可用的工作目录:install.sh 到底做了什么
3.1 挂载 ISO 与安装目录规划
拿到 ISO 之后的第一步是挂载。别急着解压到别的地方,install.sh需要看到完整的 ISO 目录结构:
mkdir -p /mnt/speciso mount -o loop,ro cpu2017-1.1.x.iso /mnt/speciso ls /mnt/specisols出来应该能看到install.sh、benchspec、tools、Docs这些顶层条目。如果只看到一个巨大的镜像文件,说明你挂错了对象——有些发行版会把 ISO 用其它工具再包一层,先确认文件类型。
安装目录我习惯放在数据盘而不是系统盘,因为后面编译产物会把目录撑得很大,而且需要频繁读写。安装路径里不要有空格和中文,SPEC 的脚本对路径的处理在有些环节不够健壮,空格会引发一些很难定位的问题。我一般用/opt/cpu2017或者数据盘下的/<datadisk>/cpu2017。
顺带说一句-o loop,ro里的ro:只读挂载能防止后续误操作改到 ISO 内容,同时也能避免某些系统在 ISO 被写之后校验失败。
3.2 install.sh 的交互过程与我没有选默认值的地方
进到挂载点直接执行:
cd /mnt/speciso ./install.sh脚本会先打印一段欢迎信息,然后逐项问你问题。核心是两三个:
第一个是安装目标目录。默认值是当前目录下的cpu2017,如果你在/mnt/speciso里执行,那就是把文件复制到/mnt/speciso/cpu2017,这显然不合适。我一般在这里手动输入/opt/cpu2017。如果目标目录已经存在,脚本会提示覆盖还是退出,覆盖会清掉你之前的 cfg 和编译产物,所以升级版本时千万别直接覆盖,换个新目录。
第二个是是否安装/构建工具链。SPEC 在 ISO 里带了预编译好的specmake、specinvoke、specdiff、specperl等工具,脚本会问你沿用现有的还是重新构建。除非你的平台比较冷门(比如某些非 x86 架构),或者预编译工具在你的系统上跑不起来,否则沿用预编译版本是最省事的。重新构建需要系统里有一套能用的编译环境,构建失败的话脚本会留下一个半成品目录,比直接用预编译还麻烦。
安装过程本质上是把 ISO 里的内容复制到目标目录,同时根据你的平台调整一些脚本里的路径变量。耗时主要看磁盘速度,几个 G 的内容,NVMe 上通常一分钟内完事。装完脚本会打印一句提示,告诉你接下来怎么做——通常是让你source shrc。
3.3 装完之后的目录地图:benchspec、bin、config、result
装完以后一定要先花五分钟把目录结构看明白,后面所有操作都在这几个目录之间来回跳:
cd /opt/cpu2017 source shrc lsshrc是环境脚本,执行之后 PATH 里会多出安装目录的bin,同时会设置几个 SPEC 相关的环境变量。每次开新终端都要重新 source,忘了就会遇到"命令找不到"的低级问题。
目录分工大致是这样:
| 目录 | 作用 | 你会不会常去 |
|---|---|---|
bin/ | runspec、specmake、specperl等命令 | 每次跑分都会用到 |
benchspec/CPU/ | 各基准程序的源码、数据集、编译目录 | 排查编译错误时必去 |
config/ | 示例 cfg 文件,你写的 cfg 放这里 | 配置阶段常去 |
result/ | 所有输出:.txt、.csv、.html、.log | 跑完必看 |
tools/ | 工具链和平台相关脚本 | 基本不用管 |
Docs/ | 用户手册和 run rules | 遇到规则问题才翻 |
benchspec/CPU/下面每个基准程序目录里,src/是源码,data/是数据集,build/是编译产物,exe/是可执行文件,run/是运行时的临时目录。编译报错时的日志不在你们以为的地方——它藏在build/build_base_<ext>-<平台>.0000/里面,而且每个基准程序一个目录,得自己去找。
4. cfg 文件才是真正的操作台
4.1 从 Example 配置抄一份骨架
每次跑分都要指定一个 cfg 文件。安装自带的config/目录里有一堆Example-linux-x86-gcc-*.cfg,直接拿其中一个改是最快的路径,但有几个字段必须动,否则要么跑不起来,要么跑出来的结果没有任何意义。
先看一眼有哪些示例:
ls config/Example-*gcc*挑一个名字里有x86_64和gcc的复制成自己的文件:
cp config/Example-linux-x86_64-gcc.cfg config/my-gcc.cfg不要直接改示例文件——那是对照用的参考,改乱了以后遇到问题没有 baseline 可比。自己的配置统一放到config/下,命名带上前缀方便识别。
cfg 的语法有两层:顶层是一行一个键 = 值的全局设置,比如iterations、label、output_format;然后是分节,分节名形如default=default=default:,三段分别对应 benchmark、tune、extension 的匹配条件。default是通配符,所以default=base=default:表示"所有基准程序、base 调优、任意 extension"。分节内的键值会覆盖全局设置,这就是针对单个基准程序定制编译选项的机制。
4.2 编译选项怎么定:OPTIMIZE 与 EXTRA_* 的分工
编译选项分两层,理解这个分工能帮你少走很多弯路。第一层是OPTIMIZE、COPTIMIZE、CXXOPTIMIZE、FOPTIMIZE,分别对应通用、C、C++、Fortran 的优化开关;第二层是EXTRA_OPTIMIZE、EXTRA_COPTIMIZE等,它们会被追加在对应开关之后。
这个"追加"特性非常关键。因为很多基准程序的spec定义里已经硬编码了一些必需开关(比如某个宏定义、某个兼容性标志),如果你在OPTIMIZE里写一套完全不同的选项,可能把那些必需开关冲掉;而用EXTRA_COPTIMIZE追加,就不会覆盖原有内容。我自己的习惯是:基础选项写在OPTIMIZE系,针对单个程序的微调一律用EXTRA_*系。
另一个必须提的开关是-fno-strict-aliasing。SPEC 的不少源码写于严格别名规则收紧之前,开高优化级别后容易被优化器做出错误假设,表现为编译通过、运行崩溃或者数值校验失败。base 场景下直接全局加上它,代价是损失一点性能,换来的是稳定性。
基地址模型也很常踩。600.perlbench_s这类程序在某些平台上需要-m64之类的开关,SPEC 提供了PORTABILITY系列键专门放这些兼容性选项,它们会在合适的位置被加入编译命令。示例 cfg 里已经帮你填好了大部分,不要嫌麻烦删掉,那些看起来没用的宏定义往往就是某个平台能编过的唯一原因。
4.3 一份可以直接改改就用的 cfg 样本
下面这份是我在 x86 服务器上常用的骨架,字段名和结构与官方示例保持一致,你按自己的路径和编译器版本替换即可:
# my-gcc.cfg action = run tune = base ext = gcc12 output_format = all label = internal-test iterations = 1 size = test default=default=default: CC = /usr/bin/gcc CXX = /usr/bin/g++ FC = /usr/bin/gfortran OPTIMIZE = -g -O2 -fno-strict-aliasing COPTIMIZE = -O3 -march=native -fno-strict-aliasing CXXOPTIMIZE = -O3 -march=native -fno-strict-aliasing FOPTIMIZE = -O3 -march=native EXTRA_LIBS = -lm几个细节值得说明。ext是扩展名,它决定编译产物目录的后缀(build_base_gcc12-<平台>.0000),不同的ext值可以让你在同一份源码上并存多套编译产物,方便对比不同编译器或不同开关的效果——这是我用得最多的一个字段,做编译器版本横评的时候特别有用,改一次ext就等于开一条新赛道,互不干扰。
-march=native在单机自测时很方便,但它会让结果绑定到具体这台机器,换一台 CPU 微架构不同的机器重跑,数字就不可比了。如果要做跨机型对比,把这个开关换成明确的-march=<具体微架构>,比如-march=skylake-avx512或者-march=znver3。
EXTRA_LIBS = -lm是给数学库兜底,某些精简系统上不加会链接失败。至于针对单个基准程序的微调,写法是这样:
605.mcf_s=base=default: EXTRA_COPTIMIZE = -fno-tree-loop-vectorize分节名里第一个字段可以直接写 benchmark 编号,匹配到就生效。调优阶段最耗时间的就是这一块,跑一个基准程序、看一次结果、改一个开关,来回几十轮很正常,所以只建议在需要 peak 分数的时候折腾。
5. runspec 的用法:从冒烟测试到正式出分
5.1 先用 test 尺寸把链路跑通
配置写完,第一件事绝对不是直接上 ref 跑全套,那样出了问题你要等几个小时才能看到报错。先用 test 尺寸跑单个基准程序:
cd /opt/cpu2017 source shrc bin/runspec --config=my-gcc.cfg --size=test --tune=base \ --iterations=1 --noreportable 605.mcf_s这里三个参数值得解释。--size=test用的是最小的数据集,几分钟就能出结果;--noreportable表示这次运行不追求"可用于正式报告"的合规性,它允许你用小数据集、少迭代次数、甚至并行跑多个程序,代价是结果不能拿出去当正式成绩;605.mcf_s指定只跑这一个基准程序,用来验证编译链路。
选605.mcf_s也是有讲究的。它是整数套件里比较"干净"的一个——C 语言、依赖少、编译快,能最快暴露编译器路径配错、specmake找不到、数据集缺失这类基础问题。等它跑通了,再回到全量运行,成功率会高很多。
如果这一步就报错了,别怀疑配置逻辑,先看三件事:gcc路径是不是绝对路径且真实存在、benchspec/CPU/605.mcf_s/下面有没有数据、result/目录有没有写权限。我遇到过一次跑不动的原因纯粹是挂载点用了noexec,二进制根本没法执行。
5.2 单跑一个 benchmark 定位问题
冒烟测试过了之后,建议再单独跑一个浮点基准程序,因为浮点套件会用到FC和 Fortran 运行时,整数套件跑通不代表浮点能跑:
bin/runspec --config=my-gcc.cfg --size=test --tune=base \ --iterations=1 --noreportable 603.bwaves_s这一步能暴露gfortran路径错误、libgfortran缺失、Fortran 优化开关不兼容这类问题。我建议整数和浮点各挑一个跑通,再去打全套,这比直接跑全套省下的时间多得多。
跑完之后用--action=validate单独做一次正确性校验也是好习惯:
bin/runspec --config=my-gcc.cfg --size=test --tune=base \ --action=validate 603.bwaves_svalidate只检查已有可执行文件的输出是否与期望值一致,不重新编译不重新运行,速度很快。它回答的问题是"我改的编译选项有没有把结果算错",这在调-O3、-ffast-math这类激进开关时特别重要——性能提升可能只是因为你把精度算没了。
5.3 全量 ref 运行的命令与耗时预期
正式的运行命令是这样:
bin/runspec --config=my-gcc.cfg --size=ref --tune=base \ --iterations=3 --reportable --output_format=all intspeed--size=ref是唯一被允许用于正式成绩的数据集,--iterations=3会跑三轮并取中位数,--reportable打开合规检查(要求 ref 尺寸、至少三轮、配置文件完整),--output_format=all让结果同时输出 txt、csv、html 三种格式。最后一个参数是套件名,可以写intspeed、fpspeed、intrate、fprate,也可以一次写多个,或者写all。
耗时要有心理准备。单套件 ref 跑三轮,在主流服务器上通常需要几个小时;四个套件全跑一遍,一整天是很正常的。别在跑到一半的时候去干别的重活——SPEC 对运行环境很敏感,同一台机器上并行做别的事情会直接影响测量结果,尤其是你正在做横向对比的时候。
跑 rate 还要额外指定副本数:
bin/runspec --config=my-gcc.cfg --size=ref --tune=base \ --iterations=3 --reportable --copies=32 intrate--copies的取值要和你机器的物理核数对齐,超了会导致资源争抢,分数不升反降。如果不想手动数核,可以用--copies留空让 SPEC 自己判断,但自动判断在某些容器环境下不准,我还是倾向手动指定。
5.4 跑完以后结果文件怎么看
所有输出都在result/下,文件名形如CPU2017.<时间戳>.<label>.txt。txt 是给人看的,里面是每个基准程序一张表:
| 列名 | 含义 |
|---|---|
| Benchmark | 基准程序编号和名字 |
| Ref Time | 参考机耗时,由套件固定给出,你不要去改它 |
| Run Time | 你这台机器的实测耗时 |
| Ratio | 上两者的比值,越大越快 |
最后会给出一个几何平均值,就是SPECspeed2017_int_base这种总分。几何平均而不是算术平均,是为了避免某个特别快的基准程序把总分拉飞,所以你看到的分数量级和单个 ratio 是同一档的。
.csv是给脚本解析用的,做自动化报告的时候用得上;.html适合发给别人看;还有一个.log文件记录了整个运行过程的细节,编译命令、运行命令、环境变量都在里面。排查问题时.log才是第一现场,txt 里只告诉你失败了,log 里才有失败前最后执行的命令是什么。
6. 我踩过的坑,以及每个坑的排查顺序
6.1 编译阶段:高版本 GCC 把警告当错误
最典型的表现是编译到一半停下来,报错信息里出现error:但紧接着的内容看起来像是提示而不是错误。这几乎可以断定是新版 GCC 把某个诊断默认提升为 error 了。定位方法很直接:在benchspec/CPU/<bench>/build/build_base_<ext>-<平台>.0000/里找到make.out或者类似的日志文件,里面完整记录了失败那一次的命令行。
不要急着去改源码,源码是受校验的,改了之后validate会失败。正确的解法是在 cfg 里针对这个基准程序加抑制开关:
620.omnetpp_s=base=default: EXTRA_CXXOPTIMIZE = -Wno-error -Wno-deprecated-declarations-Wno-error是通用解法,把"警告当错误"这个行为关掉,代价是可能漏掉真正的隐患,但在跑分场景下这个取舍是划算的。更稳妥的办法是换成老版本 GCC,从根上避开这些问题。
6.2 链接阶段:找不到 libgfortran 和 libm
编译通过但链接失败,报cannot find -lgfortran或者undefined reference to ...,基本都是运行库缺失或者路径不对。先用一条命令确认库在不在:
ldconfig -p | grep -E 'gfortran|libm\.so'如果输出里没有libgfortran.so,装对应的包就好,注意包名在不同发行版里不一样,有的叫libgfortran5,有的叫libgfortran,有的是跟着gcc-gfortran一起装的。如果库存在但链接器找不到,说明库路径不在默认搜索路径里,在 cfg 的LDFLAGS里补上-L<库路径>,或者在 cfg 里显式指定FC的绝对路径——SPEC 有时候会根据FC的位置去反推库路径,把gfortran写成绝对路径能顺带解决一部分链接问题。
还有一种情况是 32 位库缺失。虽然现在基本都跑 64 位,但如果 cfg 里基地址模型没配好,编译器可能尝试走 32 位路径,报出来的错看起来像源码问题,实际是缺库。检查一下PORTABILITY里的-m64之类的开关有没有被改掉。
6.3 运行阶段:OOM 被 kill 和结果 invalid
跑 rate 的时候最容易遇到进程被系统杀掉。日志里的表现是某个副本突然消失,或者出现Killed字样。原因通常是内存不够,dmesg里能看到 OOM killer 的记录。解法有两个方向:降低--copies,或者给机器加内存。别指望 swap 能救场,一旦开始换页,跑出来的时间就没有参考价值了,即使没被 kill 也该判无效。
另一个高频问题是结果被判 invalid。常见触发条件有几个:
- 没有满足
--reportable的前提(用了 test 尺寸、迭代次数不够) - 运行过程中系统负载发生了变化(有人登进来干了别的活)
- 机器名、CPU 信息在运行过程中发生了变化(容器环境下偶尔会出现)
- 数值校验没通过(改了大量浮点优化开关)
排查 invalid 的顺序是:先看 log 里 harness 给出的具体无效原因,那条信息通常很明确;再去核对 cfg 里的iterations和size;最后才怀疑环境和源码。我遇到过的最坑的一次 invalid 是因为跑分期间主机名被 DHCP 改了,log 里只写了一句结果校验失败,找了半天才发现。
6.4 环境阶段:PATH、noexec 挂载与权限
这类问题在冒烟测试阶段就该暴露。三个高频点:
PATH 问题。忘了source shrc会表现为runspec: command not found,或者更隐蔽一点——specmake找到了系统里那个同名或者功能类似的make,编译出来的行为不对。养成每次开终端先source /opt/cpu2017/shrc的习惯,或者把这一行写进~/.bashrc。
noexec 挂载。有些系统的安全策略会把某些挂载点设成noexec,ISO 挂上去之后二进制文件无法执行,表现为一个非常含糊的权限错误。用mount | grep speciso看看挂载选项里有没有noexec。
权限问题。install.sh需要往目标目录写文件,如果你的账号对目标目录没有写权限,脚本会在中途失败并可能留下不完整的安装。不要用 root 装完再用普通用户跑——result/和build/目录的属主会不对,普通用户没法写。要么全程用同一个账号,要么装完把目录属主改对。
7. 分数稳定下来靠的是系统侧,不是编译器
7.1 频率、SMT、NUMA 三个开关的实际取舍
同一个平台、同一份 cfg,跑两遍差个百分之几是很正常的,但如果差了百分之二十,那一定是系统侧没管住。频率是第一位的:
cpupower frequency-set -g performancepowersave或者ondemand策略下,CPU 会在空闲时降频,而 SPEC 的负载有明显的阶段性,降频会让测量结果忽高忽低。设成performance之后,还要看具体情况决定要不要关睿频——睿频带来的提升在散热条件不同的机器上不一致,做横评时关掉更公平,做单机极限性能测试时开着更接近真实使用。我自己的做法是做横评时关、出内部性能上限时开。
SMT 的取舍前面提过了:跑 rate 关掉,跑 speed 看情况开着。NUMA 是第三个开关,多路机器上如果进程在 A 节点、内存在 B 节点,跨节点访问的延迟会明显拖慢结果。做法是运行前绑定:
numactl --cpunodebind=0 --membind=0 bin/runspec --config=my-gcc.cfg \ --size=ref --tune=base --iterations=3 --reportable --copies=16 intrate如果要做全机测试,就让副本均匀铺满所有节点,用--copies等于总物理核数,同时关掉自动 NUMA 平衡,避免运行中内存在不同节点之间漂移。
7.2 ratio 和 geomean 的读法
拿到结果之后,最容易被误读的是两件事。第一是把不同套件的分数直接比大小:SPECspeed2017_int_base和SPECrate2017_fp_base是两个完全不同的量纲,放在一起比较没有任何意义,就像拿"每秒能送多少件快递"和"送一件最远要多久"比大小。
第二是忽略 base 和 peak 的差别。你跑的是 base,看到别人报的是 peak,两者差个百分之十几甚至几十都正常,直接对比就是在拿苹果比橘子。看别人的成绩时先确认四个信息:哪个套件、base 还是 peak、副本数或线程数多少、机器是什么配置。缺了任何一个,那个数字就只能当参考,不能当结论。
几何平均的含义也要清楚:它是所有基准程序 ratio 的连乘开方。这意味着一个特别差的 benchmark 会把总分明显拉低,而一个特别好的 benchmark 拉高总分的效果有限。所以分析结果时要往下看单个 benchmark 的 ratio,找到拖后腿的那几个,往往能定位到具体问题——比如某个程序的内存访问模式对你的 NUMA 配置格外敏感。
7.3 除了跑分,这套东西还能拿来干什么
很多人跑完一次就把它丢在一边了,实际上这套基准在几个场景里挺有用。编译器升级验证是我用得最多的:同一个平台、同一份 cfg,只改CC的路径,跑一遍 base,就能看出新版本编译器在这个工作负载上到底带来了多少收益,比看 release notes 靠谱得多。前面提到的ext字段在这里就派上用场了,两套编译产物可以并存对比。
服务器选型也常用到。采购前拿两台候选机器跑同一套 base 配置,重点看 intrate 和 fprate,因为真实业务里并发吞吐往往比单任务延迟更接近实际体感。这时候要注意把频率策略、SMT、NUMA 全部对齐,否则测出来的差异可能来自配置而不是硬件。
还有一个偏冷门但实用的用法:用它来验证系统调优的效果。改一个内核参数、换一种内存插槽布局、调整 BIOS 里的电源策略,跑一遍 test 尺寸的 rate 就能看出趋势。test 尺寸虽然不能出正式成绩,但做相对比较够用了,而且快很多。我一般用--size=test --noreportable --copies=<核数>做这种快速验证,一轮下来十几分钟,改一次参数跑一次,迭代效率比等 ref 快得多。
最后分享一个小技巧,关于 cfg 的版本管理。cfg 文件是纯文本,跑分结果又高度依赖它,所以我会把每次正式跑的 cfg 连同结果目录一起归档,文件名里带上日期和关键参数。过几个月回头想复现某个数字的时候,你会感谢当时多花的那一分钟。