这一章在做什么

基准测试很容易做错:工具总会给出一个数,可这个数量到的常常不是你以为的东西。这一章教你别被数字骗:跑的时候开着观测工具,确认瓶颈到底在哪(主动基准测试);分清微基准、模拟和回放各能说明什么;一档一档加负载,吞吐和延迟一起看;先用乘法核对结果有没有超过物理上限;多跑几次,看波动有多大。最后是一份拿到任何跑分都能问的问题清单。

你以为在测盘,其实测的是内存 基准程序:write(fd, buf, 4096) 系统调用、VFS 页缓存(内存) 块设备层、IO 调度 磁盘:eMMC、SSD 测 A:工具说自己在「测磁盘写入」 量到 B:数据拷进内存就返回了 内核过几秒才回写到盘,不在计时里 说成 C:「这块盘能写 1.6 GB/s」 跑的时候开着 iostat:设备那一行的 w/s 接近 0,说明这个数和盘没关系。
本站画的示意,数字是编的。这类错误书里叫「随手测」:工具本身没坏,坏在没人去核对它到底量了什么(§12.1.3)。

什么时候翻这一章

这一章不讲新工具,用的都是前面各章的:CPU 看第 6 章,磁盘看第 9 章,网络看第 10 章。扩展性模型(Amdahl、USL)的出处在 §2.6,均值、标准差、变异系数在 §2.8。

先用中文过一遍

随手测和主动测:数字要带着「卡在哪」一起交

写过 C++ 微基准的人多半遇到过:改了一行,基准快了 30%,再跑一次又回去了;或者一个循环跑出 0.0 纳秒。这些都是基准测试最常见的失败:工具量到了某个东西,只是不是你想量的那个。书里把它概括成一句话,本页在「值得停下来的句子」里引了原文:测的是 A,实际量到 B,结论却说成 C。

§12.1.3 列了十六种失败。本站把它们连同后面几节讲到的坑归成三类,方便对照自己:量错了对象(测盘测成了页缓存、测服务测成了压测客户端、编译器把被测代码删了);环境不对(测试机没按生产调优、两次测试之间不止一个因素变了、邻居和定时任务在捣乱);读错了结果(只跑一次、只看平均值、没看错误率)。还有几种是厂商和市场部的事:为跑分专门改产品(benchmark specials)、只公布对自己有利的配置。

对策只有一个:主动基准测试(active benchmarking)。基准跑着的时候,用这本书前面讲的工具去看系统:哪个资源满了,CPU 时间花在谁身上,盘到底动没动。作者检查清单的第一问是「为什么不是两倍」:吞吐停在 12 万,是什么挡住了它到 24 万?答出来,你就知道了瓶颈;如果瓶颈不是你想测的那个部件,这个结果就不该拿去下结论(§12.3.10)。顺带的好处是,这时系统上跑的是一个你知道的负载,正好用来检验自己会不会读这些工具。

同一个基准,两种跑法 被动 跑基准,等它结束 做成幻灯片 「12 万次/秒」 主动 跑基准,一直开着工具 pidstat iostat perf 为什么不是两倍? 12 万次/秒卡在压测端的单个线程 被动:只有一个数,没人知道它为什么停在这里。 主动:一个数,加上挡住它的那个东西;挡住它的不是被测对象,这个数就作废。
本站画的示意,场景是编的。书里把「跑完再看」叫被动基准测试(§12.3.1),当作反面的对照。

微基准、模拟、回放:各能说明什么

按负载像不像真实情况,基准分三种。微基准(micro-benchmark)只测一种操作:一种系统调用、一种 IO、一个函数。好处是简单,结果差了能很快找到原因;代价是它和你的真实负载之间隔着一层映射,要自己判断哪一两个维度和你有关(§12.2.1)。模拟(simulation,也叫宏基准)按生产负载的特征造请求,比如按测出来的比例混合读写;它能带出组件之间的相互影响,但容易只模拟平均值、漏掉突发(§12.2.2)。回放(replay)把抓到的真实请求重放一遍,听起来最真,可一旦新系统的延迟变了,录下来的客户端不会像真实客户那样跟着反应,回放的层次选错了还会测错对象(§12.2.3)。

设计微基准时,书里说每一项测试都还要再乘上两个维度。一是工作集大小(working set size):数据远小于内存,测的是缓存和软件路径;远大于内存,才开始测到盘。二是线程数:单线程看单核速度,多线程看整机上限。只测理想条件的叫「晴天测试」,还要补上有竞争、有干扰、负载起伏的「雨天测试」(§12.2.1)。自己写微基准,书里推荐 C,因为它和实际执行的东西对得上;但要提防编译器把结果没用到的计算整个删掉,跑的时候用别的工具确认它真的在干活,必要时看反汇编(§12.3.6)。下面的第一个动手实验就是这个坑。

负载渐增:吞吐和延迟一起看

想知道系统最多扛多少,最直接的办法是一档一档加负载(ramping load):每加一个线程或一批客户端,记下吞吐,直到不再上涨,画出来就是扩展性曲线(§12.3.7)。曲线可以直接看形状,也可以拿模型去套:Amdahl 定律描述有一段工作只能串行时,曲线逐渐变平;USL(通用扩展性定律)再加一项线程之间互相同步的开销,曲线过了峰值会往下掉(§2.6.3、§2.6.4)。下面的模拟器就是这三条线。

只看吞吐不够。接近上限时请求开始排队,延迟会先慢慢涨、再陡地涨上去。硬加负载,吞吐也许还能多挤出几个百分点,但那时每个请求都在排长队,这个最高点对用户没有意义。书里的建议是记延迟分布,问一句这个延迟客户能不能接受,报的是延迟还在可接受范围内的那一点。

逐步加并发:吞吐和延迟一起看 吞吐 p99 延迟上限(例:p99 50 ms) 报这一点:吞吐,加上当时的 p99 再往后:吞吐只多几个百分点 延迟成倍涨 并发数(线程、连接)→
本站画的示意,曲线形状是编的。吞吐曲线变平的地方,排队已经开始;拿最高点去宣传,等于承诺了一个用户无法接受的延迟。

先算上限,再看波动:两道算术

合理性检查(sanity check)就是做乘法:结果需要哪个部件跑多快,有没有超过它的物理上限(§12.3.8)。换成读者的场景:一块 ARM 板只有百兆网口,100 Mbit/s 除以 8 是 12.5 MB/s,扣掉协议头实际到不了这么多;如果有人说在这块板上测出 NFS 读 30 MB/s,不用看任何细节就知道,读的是客户端缓存,请求根本没走网络。书里说,一旦发现结果超了某个上限,多数时候最后都会查出这个结果是假的。

第二道算术是波动。同一个基准连跑十次,结果不会一样:别的进程、中断、CPU 频率、缓存冷热都在扰动它。只跑一次,你不知道它落在分布的哪里;两个版本各跑一次,差 3% 可能只是噪声。书里给的办法是跑得够久(至少不短于一秒)、多跑几次看标准差,比较两组结果时要知道误差范围(§12.1.3、§12.3.9)。变异系数(标准差除以均值)把波动变成一个数,两个版本差的百分比还没有它大,就很难说谁快(定义见 §2.8.4,后半句是本站的经验说法)。

两个版本各跑 10 次:差别有没有大过波动 情形一 AB 均值只差 2.6 ms,每组自己就差出十几 ms:分不出谁快 情形二 AB 均值差 4 ms,每组只散开 2 ms:可以说 B 快了约 4% 90 ms95100105110115
本站画的示意,每个点是一次运行的耗时(编的)。只跑一两次时,你看到的就是从这些点里随便抽的一两个。
英文中文一句话
micro-benchmark微基准测试只测一种操作的人造负载,简单、可重复,要自己映射到真实负载 §12.2.1 术语页
macro-benchmark, simulation宏基准测试,模拟按生产负载的特征造请求,测整个系统 §12.2.2
trace replay回放重放抓到的真实请求;客户端不会对新系统的延迟作出反应 §12.2.3
industry-standard benchmark行业标准基准TPC、SPEC 这类机构定义的基准,结果要公开配置 §12.2.4
passive benchmarking被动基准测试跑完再看,只收数字不做分析;书里当作反面方法 §12.3.1
active benchmarking主动基准测试跑的时候用观测工具确认测的是什么、卡在哪 §12.3.2 术语页
casual benchmarking随手测测 A、量到 B、说成 C,不核对工具到底量了什么 §12.1.3
benchmarking the benchmark测成了基准程序自己结果被压测工具本身限住,常见原因是它只有一个线程 §12.1.3
benchmark specials跑分特供为某个流行基准专门做的优化,真实客户用不上 §12.1.3
ramping load负载渐增一档一档加负载直到吞吐不涨,同时记延迟 §12.3.7
sanity check合理性检查用乘法核对结果有没有超过网口、盘、总线的上限 §12.3.8
working set size工作集大小被访问的数据有多大;比内存小,测到的多半是缓存 §12.2.1 术语页
sunny day / rainy day testing晴天测试 / 雨天测试只测理想条件,对比加上竞争、干扰和负载起伏 §12.2.1
coefficient of variation变异系数标准差除以均值,用一个数表示波动 §2.8.4 术语页

方法:按什么顺序查

§12.3 开头有一张方法表,九种方法里只有被动基准测试是反面例子,其余都围着同一件事:别只收数字,要分析。下面是本站按「做一次基准测试」的先后排的七步,前三步决定这个测试值不值得跑,后四步决定结果能不能信。

  1. 这个测试和我的问题有关吗?

    先写下要回答的问题,再看真实负载长什么样:读多还是写多、IO 多大、工作集比内存大还是小、并发多少。测一个生产里根本不走的路径,数字再漂亮也没用。§12.3.5§12.1.3

  2. 基准程序本身读得懂吗?

    优先用短小、开源、读得完的工具,或者自己写一个只做这一件事的。自己写的 C/C++ 微基准,要用掉计算结果,免得被编译器删掉。§12.3.6

  3. 跑的时候,瓶颈在哪?

    开着 pidstat -u -t 1、mpstat -P ALL 1、iostat -xz 1:被测对象有没有哪个资源到了 100%;压测客户端有没有一个核先满;盘到底动没动。都没满,说明你还没把系统推到头。再用 perf record 看时间花在谁的代码里。§12.3.2§12.3.3§12.3.4

  4. 负载加到多少就该停?

    一档一档加并发,每档记吞吐和延迟分布(p50、p99)。吞吐变平、延迟陡增的地方就是上限,报延迟仍可接受的那一档。§12.3.7

  5. 这个数在物理上可能吗?有没有出错?

    把吞吐换算成字节每秒,和网口、盘、内存带宽比;看压测工具报的错误数和超时数。错误请求通常返回得很快,会把平均延迟拉低、吞吐抬高。§12.3.8§12.1.3

  6. 再跑一次还是这个数吗?

    至少跑五到十次,报均值和标准差(或中位数和分布)。比较两个版本,差别要明显大过波动;用 perf stat -r 10 顺手拿到误差范围和指令数。§12.3.9

  7. 交结果时附上了什么?

    数字之外,写清楚配置、跑了多久、跑了几次、瓶颈是什么,以及哪些假设还没来得及核实(比如「假设这个测试真的走到了盘」)。没有分析的裸数字,书里认为多半是一次调查的开始,而不是结论。§12.1.3§12.3.10

拿到别人的跑分、又摸不到那台机器时,本站从 §12.4 的问题里挑出最常用的几组:它测的负载和我的像不像;被测的是一台机器还是一个集群,多少钱;跑了多久、跑了几次,报的是平均值还是峰值,波动多大;成功和失败各占多少;瓶颈是什么,有没有用别的工具确认过;存储类的再问工作集多大、缓存在哪一层命中;网络类的再问拓扑和调过哪些协议栈参数。

工具速查

想知道敲什么看哪里节
压测时谁占着 CPU,压测工具自己是不是瓶颈pidstat -u -t 1
mpstat -P ALL 1
客户端某个线程 %CPU 接近 100%,就是在测客户端;被测进程 %system 远大于 %usr,时间耗在内核(锁、系统调用)§12.3.2
线程是不是在抢锁、频繁睡下又醒来pidstat -wt -p PID 1每个线程的 cswch/s(主动切换)上万,多半在锁上睡§12.3.2
磁盘基准到底碰没碰到盘iostat -xz 1设备的 r/s、w/s、%util;基准报几百 MB/s 而这里接近 0,测的是页缓存§12.3.2
程序实际发了哪些系统调用、每次多大strace -c -f ./bench
strace -e trace=write -p PID
调用次数和每次的字节数;strace 开销很大,开着它跑出来的成绩要作废§12.3.2
时间花在被测对象还是基准自己身上perf record -F 99 -g ./bench
perf report --stdio
排在前面的函数属于谁;没有 root 只看得到用户态,内核里的时间要对照 pidstat 的 %system§12.3.3
两个版本差多少,波动多大perf stat -r 10 ./bench末尾的 +- 是均值的标准误差,不是单次的波动;instructions 几乎不随机,适合比工作量§12.3.9
编译器有没有把被测代码删掉perf stat -e instructions:u ./bench
objdump -d --no-show-raw-insn ./bench
指令数少得和工作量对不上;反汇编里计时的那段循环还在不在§12.3.6
全程留底,事后对账sar -o run.sa 1
cat /proc/vmstat > before.txt
跑前跑后各存一份 /proc 计数器,相减就是这段时间的增量;sar -f run.sa 回看全程§12.3.9
网口标称多快(合理性检查用)cat /sys/class/net/eth0/speed单位 Mbit/s;除以 8 就是单向字节上限,结果超过它就是测到了缓存。虚拟网卡报的是名义值§12.3.8
两台机器之间的网络吞吐iperf3 -s
iperf3 -c HOST -t 30
单向吞吐;同时在接收端开 mpstat -P ALL 1,看是不是某个核的 %soft 先满§12.2.1
文件系统、磁盘的微基准fio --name=t --rw=randread --bs=4k --direct=1 --size=1g --runtime=30IOPS 和延迟百分位;--direct=1 绕过页缓存,不加就是在测缓存§12.2.1
HTTP 服务的压测wrk -t4 -c64 -d30s --latency http://127.0.0.1:8080/--latency 打印延迟分布;出现 Non-2xx 或 Socket errors 先查错误,再看吞吐§12.2.2

pidstat、mpstat、iostat、sar 在 sysstat 包里,perf 在 linux-perf 包里,objdump 在 binutils 里;strace、iperf3、fio、wrk 在 Debian 上都是同名的包,sysbench(书里提到的 CPU 微基准)也是。本节的命令都不需要 root,perf 不加 -a 时只能看自己这个用户的进程。要反复比较两条命令的耗时,Debian 13 还有 hyperfine 包,它会自动多跑几次并报均值和标准差。工具名的链接到本站的工具卡:pidstat、mpstat、iostat、strace、perf、sar、iperf3、fio、wrk。

动手:在自己的机器上试

编译器删掉了你的基准,噪声藏住了你的改进

下面是一个很常见的微基准写法:把一个大数组加二十遍,算每个元素花了多少纳秒。KEEP 宏决定最后打不打印求和结果。先猜:不打印时,耗时会变成多少?

// sum.c:测「把一个数组加起来」每个元素要多久
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N (1 << 24)          // 16M 个 int,64 MB,比 CPU 缓存大得多
#define ROUNDS 20

static double now(void) {
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return t.tv_sec + t.tv_nsec / 1e9;
}

int main(void) {
    int *a = malloc(N * sizeof *a);
    for (int i = 0; i < N; i++) a[i] = i & 7;

    double t0 = now();
    long long s = 0;
    for (int r = 0; r < ROUNDS; r++)
        for (int i = 0; i < N; i++) s += a[i];
    double t = now() - t0;

#ifdef KEEP
    printf("sum=%lld  ", s);   // 用掉结果,编译器就不能把循环删掉
#endif
    printf("%.4f ns/元素\n", t * 1e9 / ((double)N * ROUNDS));
    free(a);
    return 0;
}
$ gcc -O2 sum.c -o sum_bad
$ gcc -O2 -DKEEP sum.c -o sum_ok
$ ./sum_bad; ./sum_ok
# 各跑 10 次,看指令数和误差范围
$ perf stat -r 10 -e task-clock,instructions:u ./sum_bad
$ perf stat -r 10 -e task-clock,instructions:u ./sum_ok
# 自己算 10 次的均值、标准差和变异系数
$ for i in $(seq 10); do ./sum_ok; done | awk '{x=$2; s+=x; q+=x*x; n++} END {m=s/n; sd=sqrt((q-n*m*m)/(n-1)); printf "平均 %.4f 标准差 %.4f CV %.1f%%\n", m, sd, 100*sd/m}'

本站在 16 核的 WSL2 上看到的(各截了关键几行):

sum_bad:  0.0000 ns/元素
sum_ok:   sum=1174405120  0.1284 ns/元素

sum_bad:  29,498,766  instructions:u
          0.0469 +- 0.0005 seconds  ( +- 1.06% )
sum_ok:  952,246,496  instructions:u
          0.0962 +- 0.0013 seconds  ( +- 1.34% )

第一轮 10 次:平均 0.1374 标准差 0.0170 CV 12.4%
第二轮 10 次:平均 0.1404 标准差 0.0093 CV 6.6%
  1. 0.0000 纳秒一个元素,做一下合理性检查就知道不可能:CPU 一个周期也要零点几纳秒。-O2 看到求和结果没人用,把整个计时循环删了。这台机器上换成打印结果的版本,指令数从 2950 万变成 9.5 亿,多出的 9 亿条指令就是那 3.4 亿次加法:本站看了反汇编,编译器把它向量化成每轮处理 4 个元素、每轮 11 条指令(int 加到 long long 要先做符号扩展)。
  2. 注意整程序的耗时只差一倍(0.047 秒对 0.096 秒):初始化 64 MB 数组本身就要时间。只看总时间,这个坑很容易混过去;看指令数,一眼就露馅。
  3. perf stat -r 10 报的 ±1.34% 是均值的标准误差(样本标准差除以 √10),本站另用一个已知耗时的脚本核对过。它说明「均值」有多稳,不说明单次运行的波动;单次的波动看自己算的标准差。
  4. 第一轮十次的变异系数是 12%,其中最慢的一次(0.168)比最快的(0.120)慢四成;隔几分钟再跑一轮是 6.6%:这台机器上同时有别人的任务在跑,连波动本身都在变。在这样的机器上比较两个版本,差 5% 根本说明不了什么。要比,就多跑、固定在空闲的核上(taskset -c 3),比中位数和分布,或者直接比 instructions。
WSL2
本站实测过上面的命令(Debian 13,gcc 14.2,perf 6.12),不需要 root。WSL2 的 perf 能拿到 instructions:u。
服务器
做严肃的对比前,先关掉能关的干扰:确认频率调节器(第 6 章),把基准绑到固定的核上,避开定时任务和监控采集的整点。云主机上同一实例类型的不同机器也有差别,书里建议多开几台、取中位数(§12.1.3)。
ARM 板
板子上常常没有 perf,拿不到指令数;可以用交叉编译工具链的 objdump(比如 aarch64-linux-gnu-objdump -d)看计时循环还在不在。板子的频率调节器常默认省电,多跑几次的波动往往比服务器大。

加线程,吞吐却掉了:跑的时候看一眼 pidstat

下面的程序模拟一个常见的服务结构:每个请求先在本地算一会儿(可以并行),再拿一把全局锁改一张共享的表(只能串行)。一档一档加线程,看吞吐怎么变;然后在 16 个线程时开着 pidstat 和 perf,看 CPU 都去哪了。每次最多跑 3 秒、16 个线程。

// ramp.c:N 个线程,每次先在本地算一会儿,再拿锁更新一张共享的表
// 用法:./ramp 线程数 [秒数];默认跑 1 秒,打印每秒完成的操作数
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define WORK 150             // 锁外的本地计算量
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
static unsigned long table[64];   // 共享数据:锁里要改它
static volatile int stop;
static unsigned long done_ops[64];

static void *worker(void *arg) {
    long id = (long)arg;
    unsigned x = id + 1, n = 0;
    while (!stop) {
        for (int i = 0; i < WORK; i++) x = x * 1103515245 + 12345;   // 锁外:可以并行
        pthread_mutex_lock(&mu);                                     // 锁里:只能串行
        table[x % 64] += x;
        pthread_mutex_unlock(&mu);
        n++;
    }
    done_ops[id] = n;
    return NULL;
}

int main(int argc, char **argv) {
    int nt = argc > 1 ? atoi(argv[1]) : 1;
    pthread_t th[64];
    for (long i = 0; i < nt; i++) pthread_create(&th[i], NULL, worker, (void *)i);
    int secs = argc > 2 ? atoi(argv[2]) : 1;
    sleep(secs);
    stop = 1;
    unsigned long total = 0;
    for (int i = 0; i < nt; i++) { pthread_join(th[i], NULL); total += done_ops[i]; }
    printf("%2d 线程  %8.0f 次/秒\n", nt, (double)total / secs);
    return 0;
}
$ gcc -O2 -pthread ramp.c -o ramp
# 负载渐增:1、2、4、8、16 个线程各跑 1 秒
$ for n in 1 2 4 8 16; do ./ramp $n; done
# 主动基准测试:跑着的时候看它的 CPU 花在用户态还是内核
$ ./ramp 1 3 & sleep 0.5; pidstat -u -p $! 1 2
$ ./ramp 16 3 & sleep 0.5; pidstat -u -p $! 1 2
$ ./ramp 16 3 & sleep 0.5; pidstat -wt -p $! 1 1
# 用户态的时间花在哪
$ perf record -g ./ramp 16
$ perf report --stdio --no-children

本站在 16 核的 WSL2 上看到的(各截了关键几行):

 1 线程    7664767 次/秒
 2 线程   13651311 次/秒
 4 线程   10771869 次/秒
 8 线程   10667901 次/秒
16 线程   10336982 次/秒

pidstat     %usr   %system      %CPU
 1 线程   100.00      0.00    100.00
16 线程   241.50   1236.50   1478.00

pidstat -wt(16 线程,每个线程)  cswch/s 约 13000

perf report:  59.59%  worker
              23.27%  pthread_mutex_lock
  1. 只看吞吐:2 个线程最快,再加就往下掉,16 个线程只有单线程的 1.35 倍。如果你只测了 1 和 16 两个点,会得出「多线程快了 35%」的结论,完全错过 2 线程时的峰值。
  2. pidstat 说出了原因:16 个线程占了将近 15 个核,其中 12 个多核的时间在 %system,也就是内核里。每个线程每秒主动睡下又被叫醒上万次:线程在锁上排队,靠 futex 进内核睡眠和唤醒,CPU 忙的是这件事,不是你的计算。
  3. perf 的报告里只有用户态(没有 root),pthread_mutex_lock 占两成多;内核里那一大块它根本看不到。这正是要几种工具对照着看的原因:只看 perf,你会以为锁只占两成。
  4. 单线程时锁从不冲突,走的是用户态的快路径;线程一多就开始进内核。这种「换了一种机制」的拐点,简单的扩展性模型拟合不好,下面的模拟器里可以把这五个点叠上去看。
WSL2
本站实测过上面的命令(sysstat 12.7.5,perf 6.12),不需要 root。数字受同机其他任务影响,峰值落在 2 还是 4 个线程,换个时间跑可能不同。
服务器
别在生产机上跑。线程数改成核数的一到两倍;要看锁等待在内核里花了多久,可以用第 5、13 章讲的 off-CPU 分析和 perf record -a(要 root)。
ARM 板
核少(常见 4 核),拐点会更早。没有 pidstat 时,读 /proc/PID/stat 的第 14、15 个字段(utime、stime,单位是时钟滴答),隔一秒读两次相减,也能分出用户态和内核态;busybox top 顶部那一行有全系统的 usr、sys 占比。

动手:模拟器

先猜:竞争系数 α 是 0.05、一致性系数 β 是 0.001 时,加到多少个线程吞吐最高?最高是单线程的几倍?把 β 拖到 0,曲线还会往下掉吗?再看模拟器验证。

模拟器

加并发:吞吐曲线

单线程吞吐固定为 1000 次/秒。拖动 α(有多少工作只能串行)和 β(线程之间互相同步的开销),看线性、Amdahl、USL 三条线怎么分开。点「渐增负载」一档一档地「测」,每次加一倍线程,测出来的点(和文字同色的实心点)带一点噪声;紫色的点是本站 ramp.c 的实测。

默认 α = 0.05、β = 0.001。拖动滑杆,或者点上面的几种情形。
—
USL 的峰值并发 N*
—
USL 的峰值吞吐(次/秒)
—
横轴最右端:USL 是线性的几成
—
Amdahl 的天花板 λ/α
  1. 点「一把大锁」:β = 0,USL 和 Amdahl 重合。把横轴改到 128,曲线越来越平,但永远不往下掉;天花板是 1000 ÷ 0.1 = 10000。再把 β 拖到 0.0005,看峰值出现在哪。
  2. 点「锁加数据同步」,然后连点「渐增负载」:1、2、4、8、16、32、64。前两档接近翻倍,之后越涨越少;过了 N*(约 22)再加,吞吐不涨反跌。只测 1 和 64 两档,会得出什么结论?
  3. 点「本站 ramp.c 的拟合」:五个实测点和 USL 曲线对得并不好,1 到 2 线程的跳跃模型解释不了。按 §2.6.2 的说法,模型和数据对不上的地方值得去查:要么模型不对,要么系统里发生了模型没考虑的事(这里是锁从用户态快路径换成了进内核睡眠)。

成书之后

SPEC CPU 2017 有了接班的 SPEC CPU 2026

书里
§12.2.4 列出的 SPEC 基准里,CPU 那一项是 SPEC CPU 2017。
今天
SPEC 在 2026 年 5 月发布了 SPEC CPU 2026,基准程序从 43 个增加到 52 个,同时宣布 SPEC CPU 2017 退役,过渡期到 2026 年 11 月。两代的分数不能直接比,据 ServeTheHome 的报道也没有官方换算系数。这正是书里说的那件事:基准一换代,新旧结果就没法放在一起比了。看厂商资料时先确认是哪一代。
核对于 2026-10。出处:heise《Most important server CPU benchmark gets an update after 9 years》;ServeTheHome 关于 SPEC CPU 2026 发布的报道。

SPECsfs2014 被 SPECstorage Solution 2020 取代

书里
§12.2.4 用 SPECsfs2014 作为 NFS、CIFS 文件服务的模拟负载。
今天
它的继任者是 SPECstorage Solution 2020,换了测试方法、加了 AI 图像处理和基因组两类负载,结果和 SFS 2014 不能比;SFS 2014 在 2021 年 12 月退役。虚拟化方面,SPEC 在 2021 年发布了多主机的 SPECvirt Datacenter 2021,发布时 SPEC 说单主机的 SPECvirt_sc2013 继续提供,两者都还列在现行基准里。
核对于 2026-10。出处:spec.org 的 SPEC Storage 与 SPEC Virtualization 基准页面;Blocks and Files《SPEC replaces SPECsfs 2014 benchmark with AI and genomics workouts》;SPEC 博客上 SPECvirt Datacenter 2021 的发布文章。

分节地图

这一章分四块:背景和常见失败(§12.1),基准的种类(§12.2),方法(§12.3),问题清单(§12.4)。上手只需要讲「怎样算有效」的 §12.1.2 和方法里的几节:主动基准测试、自己写基准、负载渐增、合理性检查、问题清单。十六种失败那一节很长,当清单查。

  1. There are lies, damn lies…约 300 词开篇微基准和宏基准的区别;基准的数字该怎么理解、怎么对应到自己的环境。
  2. Background约 100 词背景一句引子。
  3. Reasons约 500 词为什么做基准测试选型、调优、回归测试、容量规划等用途;云上做基准变便宜了。
  4. Effective Benchmarking约 700 词有效的基准测试好基准的几个特征;用基准时必须弄清的四件事;分析要在跑的时候做。
  5. Benchmarking Failures约 2800 词基准测试的失败十六种常见失败,从随手测到作弊。怀疑某个结果时回来对照。
  6. Benchmarking Types约 100 词基准的种类按负载像不像真实情况排成一条谱。
  7. Micro-Benchmarking约 500 词微基准测试优缺点;设计时要乘上工作集大小和线程数两个维度。
  8. Simulation约 400 词模拟按生产负载特征造请求;无状态和有状态的模拟。
  9. Replay约 200 词回放重放真实请求为什么也会测错。
  10. Industry Standards约 600 词行业标准基准TPC、SPEC 的来历和清单。选购整机、读厂商资料时才用得上。
  11. Methodology约 100 词方法九种方法的总表,分成实验、观察、统计几类。
  12. Passive Benchmarking约 200 词被动基准测试跑完不管的做法,以及它的结果可能错在哪几处。
  13. Active Benchmarking约 700 词主动基准测试跑的时候用工具一层层确认测的是什么。这一章最该精读的一节。
  14. CPU Profiling约 300 词CPU 剖析对基准和被测对象都做剖析,常能很快发现意外。
  15. USE Method约 100 词USE 方法要么有部件到了 100%,要么你还没把系统推到头。
  16. Workload Characterization约 100 词负载特征拿生产负载的特征和基准对比,判断相关性。
  17. Custom Benchmarks约 200 词自己写基准为什么用 C;编译器会删代码;写基准时的意外收获。
  18. Ramping Load约 600 词负载渐增一档档加负载画扩展性曲线;为什么要同时看延迟。
  19. Sanity Check约 300 词合理性检查用乘法核对结果有没有超过物理上限。
  20. Statistical Analysis约 500 词统计分析机器紧张时多收数据、事后分析;要记录波动和误差范围。
  21. Benchmarking Checklist约 200 词基准测试检查清单拿到结果先问的六个问题;第一问常常就能解决问题。
  22. Benchmark Questions约 400 词问基准的问题拿到厂商结果时可以问的长清单,按 CPU、存储、网络分组。
  23. Exercises约 100 词习题习题;本页的两个动手实验覆盖了它的实践部分。

词与短语

只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。

§0 开篇

it’s up to you
… it’s up to you to understand …
要靠你自己(去理解)
不是「随你便」,是「这是你的责任」:基准只给数,怎么解读是你的事。

§12.1.1 Reasons

benchmarketing
… marketing (also called benchmarketing).
跑分营销
benchmark 和 marketing 拼成的词,带贬义。
down the road
… difficult to change down the road.
将来,日后
不是「顺着路往下」,是时间上的「以后」。

§12.1.2 Effective Benchmarking

bring in
… then to bring in performance experts …
(把专家)请来
long predates
This quote long predates flame graphs …
远早于
long 是副词「早就」:这段话比火焰图早出现很多年。

§12.1.3 Benchmarking Failures

fire-and-forget
… not a fire-and-forget activity.
发射后不管
本是导弹术语:打出去就不用再操心。这里说基准测试不能跑起来就走开。
bogus
… may therefore be bogus.
站不住的,假的
surely must be valid
… analyze what surely must be valid.
「肯定」没问题的东西
原文 surely 是斜体,反讽:大家都觉得流行的基准不用查,作者恰恰不这么看。
I’ve tried
… stop it from happening (I’ve tried).
(我)试过了
括号里的半句自嘲:言外之意是试了也没拦住。
once upon a time
… appropriate workload once upon a time …
曾经,当年
童话的开头语,这里只是「从前」:这个基准当年测得对,现在过时了。
iron out
… strategy for ironing out perturbations is …
熨平,消除(干扰)
iron 是熨斗。把偶然的扰动「熨平」,办法是跑得更久。
as a rule
As a rule, the duration of …
一般来说,按经验
不是「规定」,是经验法则:基准跑的时间不该短于一秒。
might as well flip a coin
… might as well flip a coin.
还不如抛硬币
might as well 是「不如、倒不如」:这些基准准确度差到和抛硬币差不多。
shake out
… perhaps shaking out the worst issues …
(把问题)抖出来、清理掉
像抖落布上的灰:用户头几周就把最严重的问题找出来解决了。
may well
… may well see that you’ve posted …
很可能
well 加强语气,不是「也可以」。
undersell
… published numbers that undersold the product.
把(产品)说差了
不是「低价卖」,是「表现得比实际差」:自家人测错了自家产品。
liberally
The marketing department liberally shares an ambiguous …
大肆地,随意地
和政治上的「自由派」无关,是「不吝惜、到处」的意思。
lying by omission
… it is lying by omission.
靠隐瞒来撒谎
数字不假,但把关键条件(比如价格)略去不提。
fine print
… check the fine print for what …
小字说明
合同里印得很小、容易被忽略的条款。这里指跑分结果附带的配置细节。

§12.2.1 Micro-Benchmarking

factored out
… other components is factored out as …
被排除在外
不是数学上的「提出公因式」,是「不让它成为影响因素」。
sunny day
… has been called sunny day performance testing.
晴天(测试):只测理想条件
后面对应的 cloudy day、rainy day 是有竞争、有干扰的情况。

§12.2.2 Simulation

if not closely
… if not closely, at least closely enough …
即使不算很接近
if not X, at least Y:「就算达不到 X,至少也有 Y」,不是「如果不接近」。
flash sales
… “flash sales” on an online shopping platform …
限时抢购

§12.2.3 Replay

as good as testing in production, right?
… as good as testing in production, right?
跟在生产上测一样好,对吧?
设问句,作者马上否定了它:下一句就是「然而这有问题」。
mess things up
… subtle timing effects can mess things up …
把事情搞砸

§12.3.1 Passive Benchmarking

haunt
… poor product choices that haunt the company …
(后果)一直纠缠
本义是鬼魂作祟。选错了产品,后患无穷。

§12.3.2 Active Benchmarking

as a bonus
As a bonus, this can be …
额外的好处是
at least, misleading
… this is, at least, misleading.
往轻里说,也是误导
at least 在这里是「最起码」,暗示还可以说得更重。
served people well
… it has served people well on …
一直挺管用
作者在批评之后补一句公道话:这个工具并不特别差。

§12.3.3 CPU Profiling

single out
… is worth singling out as a methodology …
单独拿出来(讲)

§12.3.6 Custom Benchmarks

elide
… the compiler may elide simple benchmark routines …
省略掉,删掉
编译器领域的常用词(copy elision 也是它):编译器判断结果没人用,就不生成这段代码。

§12.3.7 Ramping Load

scrape past
… you manage to scrape past 1 million …
勉强越过
scrape 是「擦着、刮过」:费了很大劲才刚刚过线。

§12.3.8 Sanity Check

well in excess of
… well in excess of the 1 …
远远超过
well 是程度副词「大大地」,不是「好」。

§12.3.9 Statistical Analysis

forensic analysis
… so that forensic analysis can be performed …
事后取证式的分析
forensic 本指法医、刑侦:现场已经没了,只能靠留下的数据还原。

§12.3.10 Benchmarking Checklist

Why not double?
Why not double? Why was the …
为什么不是两倍?
不是在问「为什么不翻倍去做」,是问:什么东西挡住了它,让它停在这个数?也就是问瓶颈。

§12.4 Benchmark Questions

overclocked
Were processors overclocked? Was custom cooling used …
超频

诊断题:换成你来查

场景和输出都是本站编的。先自己判断,再点选项看解释。

这块板子的 eMMC 真有这么快吗?§12.3.2

场景:你给一块 ARM 板上的日志库写了个写入基准,每次 write 4 KB,共写 256 MB,报出 1.6 GB/s。板子的 eMMC 标称顺序写 90 MB/s。基准跑着的时候你开着 iostat:

Device     w/s     wkB/s   %util
mmcblk0    0.0       0.0     0.0
mmcblk0    0.0       0.0     0.0
(基准结束几秒后)
mmcblk0  712.0   91136.0    99.2

计时期间盘一个字节都没写,基准结束后才出现每秒 90 MB 左右的回写,这正是页缓存的样子。1.6 GB/s 比标称值高一个数量级,合理性检查也过不了。要测盘:用 O_DIRECT 打开文件,或者每次写完 fsync,或者让写的总量远大于内存;跑的时候继续开着 iostat 确认 w/s 真的起来了。压缩确实会让写入变少,但盘上看到的写入不会是 0。

QPS 停在 4.2 万,这是服务的上限吗?§12.3.10

场景:8 核服务器上的 C++ HTTP 服务,你在另一台机器上用 wrk -t1 -c200 压它,QPS 怎么加连接都停在 4.2 万。两边同时开着 pidstat:

服务端   %usr  %system   %CPU  Command
         96.0    84.0   180.0  server
压测端   %usr  %system   %CPU  Command
         61.0    38.0    99.0  wrk

问一句「为什么不是两倍」:服务端 8 个核只用了不到 2 个,压测端唯一的线程却到了 99%。这就是书里说的测成了基准程序自己,常见原因正是压测工具单线程。把 -t 加到压测机的核数,或者用多台客户端,同时继续看两边的 CPU。网络是不是瓶颈,可以把吞吐换算成字节每秒和网口速度比一比;调线程池要等确认服务端真成了瓶颈再说。

这次改动让服务快了 4% 吗?§12.3.9

场景:你优化了一个解析函数,用自己写的微基准各跑了三次,结果如下。同事问能不能在 PR 里写「快了 4%」。

旧版本   102 ms   97 ms   108 ms   平均 102.3
新版本    99 ms   94 ms   101 ms   平均  98.0

三次运行里,旧版本自己就从 97 散到 108,波动比两组均值的差(4 ms)大得多,样本这么少,这 4% 完全可能是噪声。多跑几十次后算标准差或置信区间,差别仍然明显大过波动才能下结论。取最小值是微基准里常见的做法,能去掉一部分偶然的干扰,但三个样本里挑最小的照样不稳。指令数(perf stat -e instructions:u)不受调度噪声影响,适合先确认这次改动到底少做了多少工作。

该报哪个数给负责容量规划的同事?§12.3.7

场景:你一档一档加并发连接压测一个服务,服务的目标是 p99 小于 50 ms。记录如下:

并发    QPS       p99
  32    96000    12 ms
  64    99000    35 ms
 128   101000   140 ms

从 64 到 128 并发,吞吐只多了 2%,p99 却涨了四倍、超过了目标:请求都在排队。报最高点等于承诺一个用户受不了的延迟。书里的建议是同时测延迟分布,报延迟还能接受的那一点,并把延迟一起写上。96000 过于保守,白白丢掉了余量;三档平均没有任何意义。

改完配置,QPS 翻了三倍?§12.1.3

场景:同事改了服务的一个配置,用 wrk 压测 30 秒后在群里报喜:QPS 从 3 万涨到 9 万,平均延迟从 8 ms 降到 0.9 ms。你看了一眼 wrk 输出的最后几行:

  Non-2xx or 3xx responses: 1912330
Requests/sec:  90112.45
Transfer/sec:     14.20MB

30 秒左右 9 万 QPS,大约 270 万个请求,其中 191 万个不是成功响应。错误往往在很早的地方就返回(参数校验失败、没权限、被限流),比正常请求快得多,于是吞吐和平均延迟都变「好」了。书里专门把忽略错误单列为一种失败,还举了一个更极端的例子:请求全被防火墙挡住,报出的延迟其实是客户端的超时。先看服务日志,查出是什么错误,修好后再测。

值得停下来的句子

Casual benchmarking: you benchmark A, but actually measure B, and conclude you’ve measured C.— §12.1.3。本站译:随手测:你测的是 A,实际量到的是 B,结论却说你量了 C。

三个字母对应三个环节:设计、执行、解读,每一环都可能出岔子,而且互不相干。工具可能完全正常,却量到了别的东西(测盘量成了页缓存);量对了,结论还可能说过头(微基准快了 10%,就说服务快了 10%)。读完这一章再看自己写过的基准,可以挨个问:我打算测什么,跑的时候工具说它在干什么,我最后写下的结论有没有超出它。

If you’ve spent less than a week studying a benchmark result, it’s probably wrong.— §12.1.3。本站译:如果你研究一个基准结果的时间不到一周,它多半是错的。

这话听着夸张,书里紧接着给了退路:没时间细查的话,就把没来得及核实的假设写在结果旁边,比如「假设这个测试真的走到了盘」「假设压测工具没有先到上限」。对日常开发也适用:PR 里贴的性能数字,附上跑了几次、波动多大、瓶颈在哪,就比一个裸数字可信得多。

带走的几条

一句话:跑基准的时候就去看系统,说清它卡在哪,结果才算数。

跑的时候开着工具

pidstat、mpstat 看谁满了,iostat 看盘动没动,perf 看时间花在谁的代码里。交结果时附上瓶颈。

§12.3.2
先问为什么不是两倍

答出是什么挡住了吞吐,就知道了瓶颈;瓶颈不是被测对象(常见的是压测客户端),这个数就作废。

§12.3.10
先做乘法

吞吐换算成字节每秒,和网口、盘、内存带宽比。超过物理上限,测到的是缓存。

§12.3.8
用掉基准的结果

结果没人用,-O2 会把计时循环删掉。看 perf stat 的指令数或者反汇编确认它在干活。

§12.3.6
差别要大过波动

多跑几次,报标准差或分布。perf stat -r 的 ± 是均值的标准误差,不是单次的波动。

§12.3.9
加负载时盯着延迟

吞吐变平、延迟陡增的地方是上限。报延迟可接受的那一点,并写上当时的 p99。

§12.3.7
先看错误数

错误响应返回得快,会让吞吐和平均延迟都变好看。成功率不对,其他数字都别看。

§12.1.3