1. 为什么我建议用 CPU 当秒表来测硬盘和网络
先说一个可能颠覆认知的事实:你电脑里的 CPU,一秒钟能执行几十亿次基本操作。而你的硬盘,哪怕是最新的 NVMe 固态,随机读一次数据也要消耗几十微秒。这中间的差距,不是几倍,不是几十倍,而是上万倍。
大多数人电脑用着"感觉卡",其实并不是 CPU 不够快,而是 CPU 在等硬盘、等网络。这个"等"的过程,你感知到的只是"转圈圈",却没法量化到底等了多少。如果你能钻进 CPU 的视角,用它的时钟周期去给硬盘和网络测速,你会发现:原来每次我以为"很快"的操作,在 CPU 眼里已经漫长得像等了一整天。
这篇内容不是什么高深的理论课,而是我自己做的几个测量小实验。核心思路很简单:用 CPU 内部的时钟计数器来计时,分别测量内存访问、硬盘随机读、网络请求这些操作的耗时。读完你不仅能得到一个量化的数字,更重要的是建立一种"数量级直觉"——以后你再看到某个系统设计、某个框架选型,心里会立刻浮现出"这操作要花多少 CPU 周期"的衡量标准。
适合谁看?如果你写过代码,遇到过程序卡顿、接口超时、数据库查询慢这类问题,却说不清楚到底慢在哪,那这篇内容很值得花十分钟看完。就算你没写过底层代码,只要会跑几条命令,我也尽量把所有实验步骤写明白。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 秒表从哪来:认识 CPU 内部的 TSC 时钟
2.1 CPU 自带一个纳秒级秒表
现代 x86 处理器内部有一个叫 TSC(Time Stamp Counter,时间戳计数器)的寄存器,从 CPU 上电开始,它就以 CPU 的主频频率持续递增。比如一颗 3.2GHz 的 CPU,TSC 每秒钟增加约 32 亿次。也就是说,TSC 每一次递增,代表约 0.31 纳秒。
这玩意儿最大的价值在于:它不像 gettimeofday 或者 clock_gettime 那样要经过操作系统、要处理系统调用开销,而是 CPU 硬件原生的计数器。你在用户态直接读它,开销几乎是零。拿它来做微基准测试,精度是纳秒级的。
在 Linux 上,一条 rdtsc 指令就能把它读出来。C 语言里可以这样写:
c复制#include <stdint.h>
static inline uint64_t read_tsc() {
unsigned int lo, hi;
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
读出来的值除以 CPU 主频,就得到了从开机到现在经过的秒数。测量某个操作耗时,只需要在操作前后各读一次 TSC,相减得到周期数,再除以频率就是秒。
2.2 用之前必须注意的坑
TSC 虽然好用,但直接用会踩坑,我先说清楚:
第一,指令乱序执行。现代 CPU 为了提高效率,会乱序执行指令。rdtsc 本身不阻止乱序,所以你测出的时间可能偏大或偏小。严谨的做法是用 rdtscp 指令,它会等前面的指令都执行完再读数。或者直接用 lfence 加 rdtsc:
c复制static inline uint64_t read_tsc() {
unsigned long long tsc;
__asm__ volatile ("lfence; rdtsc" : "=A"(tsc) :: "memory");
return tsc;
}
第二,CPU 频率会变。笔记本上尤其明显,负载低时降到 1.2GHz,负载高时睿频到 4.5GHz。如果 TSC 也跟着变,那测出的周期数没法换算成时间。好在现代 CPU 的 TSC 大多已经是 constant_tsc 和 nonstop_tsc,也就是恒定频率,不随主频变化。你在 Linux 上可以用 grep tsc /proc/cpuinfo 看有没有这两个标志,有的话说明 TSC 是稳定的,可以直接按标称主频换算。
第三,多核迁移问题。线程可能在 CPU 核心间迁移,不同核心的 TSC 理论上应该同步,但保险起见测量时最好绑定一个核心。用 sched_setaffinity 可以做到:
c复制#include <sched.h>
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
sched_setaffinity(0, sizeof(set), &set);
搞定了这三个坑,你就拿到了一个精度约 1 纳秒、开销几乎为零的大自然馈赠秒表。
2.3 为什么不用系统自带的计时函数
你可能会问:clock_gettime(CLOCK_MONOTONIC) 不也能测吗?实测你会发现,一次 clock_gettime 调用本身的成本大约是 20~50 纳秒,而且它在底层其实也是基于 TSC 之类的硬件时钟来做的,只是中间包裹了一层虚拟化适配和系统调用的逻辑。
如果你的测量目标本身是微秒甚至毫秒级,用 clock_gettime 完全没问题。但我们这次要测的内存访问,是几十纳秒级别的操作——用 clock_gettime 去测,就像用一把最小刻度为厘米的尺子去量一根头发丝的直径,尺子本身的厚度比头发丝还粗,量出来的数字没有意义。
所以我的结论是:测纳秒级操作,直接读 TSC;测微秒级以上,clock_gettime 够用。两把尺子配合使用。
3. 硬盘速度实测:从内存到 SSD 再到机械盘的延迟阶梯
3.1 先用 CPU 测内存访问延迟
在测硬盘之前,先测一个"参照物"——内存。因为内存是 CPU 直接打交道的存储,理解了内存的速度,后面的对比才有意义。
内存延迟的经典测法是"指针追逐":构造一个链表,让每个节点的下一个节点地址是随机分布的,然后从头节点开始不断访问 node = node->next。这样 CPU 没法预取,每一次访问都必须真实地去内存里取数据,测出来的就是真实的内存随机访问延迟。
c复制#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#define ARRAY_SIZE (64 * 1024 * 1024) // 64MB 数组
#define STRIDE 64
static inline uint64_t read_tsc() {
unsigned long long tsc;
__asm__ volatile ("lfence; rdtsc" : "=A"(tsc) :: "memory");
return tsc;
}
int main() {
// 分配内存并构造随机跳跃的指针链
uint64_t i, idx, next_idx;
uint64_t *arr = malloc(ARRAY_SIZE * sizeof(uint64_t));
uint64_t *next = malloc(ARRAY_SIZE * sizeof(uint64_t));
for (i = 0; i < ARRAY_SIZE; i++) {
next[i] = rand() % ARRAY_SIZE;
}
idx = 0;
uint64_t sum = 0;
// 预热
for (i = 0; i < 100000; i++) {
idx = next[idx];
}
uint64_t t0 = read_tsc();
for (i = 0; i < 10000000; i++) {
idx = next[idx];
}
uint64_t t1 = read_tsc();
double cycles = (double)(t1 - t0) / 10000000.0;
double ns = cycles / 3.2; // 假设 3.2GHz
printf("内存随机访问延迟: %.2f cycles, %.2f ns\n", cycles, ns);
return 0;
}
这段代码在我的机器上跑出来,内存随机访问延迟大约是 90~110 个周期,换算成时间大约是 28~34 纳秒。这个数字意味着:CPU 每次等内存把数据送回来,要白白等大约 100 个周期。而这已经是除了 CPU 缓存之外最快的存储了。
3.2 用 CPU 时钟测 SSD 随机读延迟
接下来测 SSD。我想看的是真实的"应用视角"延迟——也就是发起一次 pread 系统调用去读 4KB 数据,从调用发出到数据落到缓冲区,一共花了多少时间。
这里有个细节:系统调用本身有开销,文件系统也有页缓存,如果数据已经在页缓存里,读操作根本不会碰到硬盘,测出来的是内存速度。所以要么直接打开 O_DIRECT 标志绕过页缓存,要么先 mlock 把脏页清掉。我用 O_DIRECT 测,这样才是最真实的硬件延迟。
c复制#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
static inline uint64_t read_tsc() {
unsigned long long tsc;
__asm__ volatile ("lfence; rdtsc" : "=A"(tsc) :: "memory");
return tsc;
}
int main(int argc, char *argv[]) {
int fd = open(argv[1], O_RDONLY | O_DIRECT);
if (fd < 0) { perror("open"); return 1; }
void *buf;
posix_memalign(&buf, 4096, 4096);
int iterations = 1000;
uint64_t total = 0;
for (int i = 0; i < iterations; i++) {
lseek(fd, (rand() % 100000) * 4096, SEEK_SET);
uint64_t t0 = read_tsc();
read(fd, buf, 4096);
uint64_t t1 = read_tsc();
total += (t1 - t0);
}
double avg_cycles = (double)total / iterations;
double avg_us = avg_cycles / 3.2 / 1000.0; // 转换成微秒
printf("4KB 随机读延迟: %.0f cycles, %.2f us\n", avg_cycles, avg_us);
close(fd);
return 0;
}
如果你的环境不方便编译 C 代码,用 Linux 自带的 fio 工具也能得到同样精度的数据。我给出命令:
bash复制fio --name=randread --rw=randread --bs=4k --size=1G \
--iodepth=1 --ioengine=libaio --direct=1 --runtime=10 \
--filename=/path/to/testfile
iodepth=1 是关键,表示每次只发一个 IO 请求,模拟的是最真实的单次随机读延迟,而不是吞吐量测试。
实测数据如下:
| 存储类型 | 4KB 随机读延迟 | 相对 CPU 时钟周期(3.2GHz) |
|---|---|---|
| 内存 | 约 30 ns | 约 100 周期 |
| NVMe SSD(PCIe 4.0) | 约 40~60 us | 约 13 万~19 万周期 |
| SATA SSD | 约 100~200 us | 约 32 万~64 万周期 |
| 机械硬盘(7200 转) | 约 7~12 ms | 约 2200 万~3800 万周期 |
看到这组数据,我才真正理解了什么叫"快"和"慢"。内存访问 100 个周期,NVMe SSD 就要 13 万个周期,机械硬盘直接飙到 2000 万个周期。从内存到机械硬盘,延迟翻了大约 20 万倍。
3.3 机械硬盘为什么这么慢
机械硬盘的延迟大头不在读写,而在"寻道"。磁头要移动到目标磁道(寻道),要等盘片转到目标扇区(旋转等待)。7200 转的盘,平均旋转延迟大约 4.17 毫秒,再加上 8~10 毫秒的寻道时间,单次随机访问 12 毫秒是常态。
这个 12 毫秒在 CPU 眼里是什么概念?3.2GHz 的 CPU 能执行大约 3800 万条指令。也就是说,你等一次机械硬盘随机读的时间,CPU 可以完整跑完一个微型操作系统的启动流程。
所以每次我看到有人还在用机械硬盘当系统盘,我都会想:你以为是系统慢,其实是 CPU 每秒都在经历的"机械硬盘大堵车"拖垮了所有体验。
3.4 系统调用本身的开销有多大
写上面的测试程序时,我还顺手做了一个小实验——单独测一次 read 系统调用的固定开销。方法很粗暴:读一个已经热到页缓存里的文件,重复百万次,测量平均耗时。因为数据已经在内核页缓存里,测试结果代表的主要是"陷入内核 + 页面拷贝 + 返回用户态"的固定成本。
结果让我有点意外:一次热缓存的 4KB read 系统调用,大约耗时 500 纳秒到 1 微秒。相比纯内存访问的 30 纳秒,系统调用这层"过路费"就有 1 个数量级。这解释了为什么那些频繁做小 IO 的程序,哪怕读的是热点数据,也跑不快——它们的大部分时间都消耗在系统调用本身,而不是真正的 IO 上。
这个认知对写高性能代码影响很大:能少调用一次系统调用就少调用一次,缓冲区攒一批再写入,远好过每次都跑内核。
4. 网络延迟的真相:环回、局域网、跨地域各差多少
4.1 用 socket 自己测网络延迟
硬盘测完了,再来测网络。网络延迟的测试方法和硬盘不太一样:网络请求的延迟一般不依赖 CPU 时钟那么高的精度,因为最慢的网络延迟也在亚毫秒以上。用 clock_gettime 足够。但为了保持一致的"CPU 视角",我还是继续用 TSC 来测。
最基础的网络延迟指标是 RTT(Round-Trip Time,往返时间)。我直接写一个小小的 TCP 客户端/服务器,每次连接建立后,客户端发一个字节,等服务器回一个字节,记录一次完整往返的 TSC 周期数。
服务器端:
c复制#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(9999);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 50);
while (1) {
int conn_fd = accept(listen_fd, NULL, NULL);
char c;
while (read(conn_fd, &c, 1) > 0) {
write(conn_fd, &c, 1);
}
close(conn_fd);
}
return 0;
}
客户端:
c复制#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdint.h>
static inline uint64_t read_tsc() {
unsigned long long tsc;
__asm__ volatile ("lfence; rdtsc" : "=A"(tsc) :: "memory");
return tsc;
}
int main(int argc, char *argv[]) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
inet_pton(AF_INET, argv[1], &addr.sin_addr);
connect(fd, (struct sockaddr*)&addr, sizeof(addr));
char c = 'x', buf;
int iterations = 1000;
uint64_t total = 0;
for (int i = 0; i < iterations; i++) {
uint64_t t0 = read_tsc();
write(fd, &c, 1);
read(fd, &buf, 1);
uint64_t t1 = read_tsc();
total += (t1 - t0);
}
double avg_us = (double)total / iterations / 3.2 / 1000.0;
printf("RTT: %.2f us\n", avg_us);
close(fd);
return 0;
}
这个实验我分别测了三种场景:连接本机的 127.0.0.1(环回)、连接局域网内另一台机器、连接一个跨地域的公网服务器。结果如下:
| 网络场景 | 实测 RTT | CPU 周期数(3.2GHz) | 大致体验类比 |
|---|---|---|---|
| 环回地址(本机) | 约 30~60 us | 约 10万~20万周期 | 读一次 SATA SSD |
| 局域网同网段 | 约 0.3~1 ms | 约 100万~320万周期 | 读几十次 SSD |
| 同城数据中心 | 约 5~20 ms | 约 1600万~6400万周期 | 读几次机械硬盘 |
| 跨地域(如国内到海外) | 约 150~300 ms | 约 4.8 亿~9.6 亿周期 | CPU 已经能跑完海量计算 |
注意环回地址这个数字。它走的是 TCP/IP 协议栈,尽管数据根本不出网卡,但一次完整的 TCP 往返也需要几十微秒。相比内存访问的 30 纳秒,环回网络比内存慢了约 1000 倍。这就解释了为什么很多"本地缓存"方案用内存而不是本机 socket——哪怕通信双方在同一台机器上,走 socket 也远不如直接读内存快。
4.2 网络慢的物理本质:光速和排队
网络延迟为什么降不下来?拿跨地域网络来说,物理链路决定了光信号从一个机房到另一个机房的时间。光在光纤中的传播速度大约是每秒 20 万公里,如果两个数据中心相距 1000 公里,光走一个单程就需要 5 毫秒。一个 RTT 至少 10 毫秒,这还只是理想情况。实际网络中每个路由节点都有处理延迟,拥塞时还有排队延迟,所以实测 150 毫秒甚至更高非常常见。
这些物理限制意味着:无论软件怎么优化,跨地域网络请求的延迟下限就摆在那里。你能优化的只有协议开销和连接复用,物理链路的往返时延是永远绕不开的。
4.3 TCP 握手和 TLS 握手比想象中更贵
做一个完整的 HTTP API 请求,实际耗时的构成远比"传输数据"复杂。我测过一次完整的 HTTPS 请求延迟,用 curl 带计时参数:
bash复制curl -w "连接时间: %{time_connect}s\nTLS握手: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总耗时: %{time_total}s\n" https://example.com -o /dev/null
对一个跨地域的服务,常见结果是这样:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| DNS 解析 | 10~50 ms | 取决于本地缓存和递归服务器 |
| TCP 连接 | 0.5~1.5 RTT | SYN + SYN-ACK + ACK |
| TLS 握手 | 1~2 RTT | 取决于 TLS 版本和会话复用 |
| 首字节TTFB | 0.5~2 RTT | 服务器处理时间 + 响应传输 |
一个简单的 HTTPS 请求,如果跨地域,光握手阶段就吃掉 3 个多 RTT,加起来轻松超过 300 毫秒。而这 300 毫秒里,CPU 本身只干了极少的活儿——纯粹是在等数据包在链路上跑。
所以现在的高性能网络框架都在做连接池。HTTP 长连接、TLS 会话复用,本质目的就一个:把那些一次性的握手成本摊薄到多次请求上,让每次请求只付出纯数据传输的 RTT。
5. 从"慢"的直觉到系统设计的硬道理
5.1 一张表看穿存储和网络的全貌
把前面所有测量数据汇总到一张表里,用 CPU 周期的视角来看整个体系的性能分布:
| 操作 | 典型耗时 | 约等于 CPU 周期(3.2GHz) | 相对内存的倍数 |
|---|---|---|---|
| L1 缓存访问 | 1 ns | 3 | 0.03x |
| L2 缓存访问 | 5 ns | 16 | 0.17x |
| 内存随机访问 | 30 ns | 100 | 1x |
| 系统调用(热缓存) | 约 1 us | 约 3200 | 33x |
| NVMe SSD 随机读 | 50 us | 约 16 万 | 1600x |
| 环回 TCP RTT | 50 us | 约 16 万 | 1600x |
| 局域网 RTT | 0.5 ms | 约 160 万 | 16000x |
| SATA SSD 随机读 | 150 us | 约 48 万 | 5000x |
| 机械硬盘随机读 | 10 ms | 约 3200 万 | 33 万x |
| 跨地域网络 RTT | 150 ms | 约 4.8 亿 | 500 万x |
看这张表,几个重要的结论立刻浮现出来:
第一,L1 缓存到内存之间就差了一两个数量级。这是 CPU 设计里所有预取、分支预测、乱序执行的动机。现代 CPU 芯片里接近一半的晶体管都是缓存和调度逻辑,不是为了算得更快,而是为了少等内存。
第二,从内存到磁盘,跨越了 3~5 个数量级。凡是能把数据放内存处理的,就绝不放磁盘。这也是 Redis 这类内存数据库存在的根本原因——它把数据从"微秒到毫秒"的硬盘延迟降到了"几十纳秒"的内存延迟。
第三,网络最慢时比机械硬盘还要慢一个数量级。一个跨地域的网络请求,等一次的时间足够 CPU 做几亿次运算。这就是为什么所有分布式系统设计的第一原则都是"尽量减少网络 RTT",把一次网络交互变成一批,把同步等待变成异步。
5.2 这些数字直接对应几条工程原则
有了数量级直觉之后,很多系统设计方案就不再是"别人都这么说"的空洞教条了,而是有数字支撑的硬道理。
第一条,能缓存就缓存。内存访问 30 纳秒,SSD 访问 50 微秒,差了 1600 倍。所以一个热接口如果命中 Redis 缓存,响应时间是 1 毫秒左右;打穿到 MySQL 再随机读几次 SSD,轻松超过 10 毫秒。缓存的意义不是玄学,纯粹是拿最快的存储去挡住最慢的存储。
第二条,能批量就批量。网络一次 RTT 如果跨地域要 150 毫秒,那么请求 1 条数据和请求 100 条数据,主要耗时都花在那 150 毫秒的固定往返上。区别只在于传输 100 条数据的"净传输时间"。这就是消息队列批量消费、数据库批量写入、日志批量刷盘的全部动机——摊薄固定成本。我实践中的一个经验:把 100 条小数据合并成一次网络请求发送,总耗时和发 1 条几乎一样,吞吐量却能提升两个数量级。
第三条,能异步就异步。当慢操作动辄需要几十万甚至几百万 CPU 周期时,让一个线程在那里阻塞等待,等于让几千亿次运算能力的核心干瞪眼。异步编程的本质,是在等待慢 IO 的时间里去干别的 CPU 密集活儿。所以高并发服务器如果用同步阻塞模型,一个线程同一时刻只能服务一个请求;换成事件驱动异步模型,同样一个线程可以同时挂起几千个等待中的请求。
第四条,顺序读写远比随机读写快。机械硬盘顺序读能到 150MB/s,随机读只有 0.5MB/s,差了 300 倍。SSD 的随机读虽然快,但顺序读仍然是随机的 3~5 倍。所以日志系统设计成只追加,数据库的 WAL 先写日志再改数据页,本质都是把随机 IO 变成顺序 IO,用顺序写的高吞吐去换随机访问的灵活性。
5.3 实测一次慢接口,定位瓶颈在哪
掌握了这些数字,你可以做一件很实际的事:给自己项目的接口做一次"延迟拆解"。
我之前排查过一个慢接口,前端反馈要 2 秒才返回。我想当然以为是数据库慢查询,结果拆开一看,2 秒里有 1.4 秒消耗在网络链路上——前端页面加载时候又额外调了 7 个接口,每个接口都是串行请求,且没有连接复用。7 个串行跨地域请求,每个 200 毫秒,加起来就是 1.4 秒。
这个案例里,数据库查询其实只用了 80 毫秒,CPU 本身更是只忙了几毫秒。真正的瓶颈在网络 RTT 累积和串行请求设计。后来改造思路很简单:7 个接口合并成 1 个聚合接口,服务端并行请求下游,响应时间直接从 2 秒降到 400 毫秒。原理就是那条"批量/并发摊薄 RTT"的规律在起作用。
所以我一直觉得,做开发的人心里必须有一张"延迟预算表"。看到任何一个慢接口,第一反应不是"哪里代码写得差",而是先拿这张表去对照——慢在计算?慢在内存?慢在磁盘?慢在网络?定位到具体层级,再去针对性优化,效率能提高十倍。
6. 怎么复现这套测量:一份可直接跑的实验清单
如果你看完也想亲手验证这些数字,我建议你按下面的顺序来,避免踩我踩过的坑。
6.1 环境准备
一台 Linux 机器,最好是物理机而不是虚拟机。虚拟机里的 TSC 读取经常会经过虚拟化层,数据会偏大。如果你只有 Windows,那可能更麻烦一点,建议装个 WSL2 或者直接用真机。CPU 型号不限,但最好确认一下 constant_tsc 特性:
bash复制grep -m1 flags /proc/cpuinfo | grep tsc
看到 constant_tsc 和 nonstop_tsc 就行。如果没有,TSC 换算时间会有偏差,那就不推荐用 TSC 方案,直接用 clock_gettime 测微秒级以上的操作就好。
6.2 按顺序跑这些实验
第一,内存延迟。用前面贴的指针追逐代码,编译运行:
bash复制gcc -O2 memtest.c -o memtest && ./memtest
注意 -O2 优化一定要开,不然编译器生成的低效代码会干扰测量。最好也 mlockall 防止内存被换出。
第二,SSD 延迟。把 ssdtest.c 编译后任意指定一个测试文件路径,注意文件至少要有 500MB 大小,确保随机读能覆盖到比较宽的区域。测之前先 sync 清掉写入缓存,但 O_DIRECT 模式下页缓存基本不影响。
第三,网络延迟。先在你本机跑 TCP 服务器端,再跑客户端连 127.0.0.1,然后换成局域网 IP,再换成公网 IP。每次至少跑 100 次取平均值,网络抖动大时取中位数更合理。
第四,一次 HTTPS 请求的延迟拆解,用那条 curl 命令最省事。
6.3 常见测量误区和对应解决办法
我踩过的坑至少有这三个:
第一个误区:直接用 clock_gettime 测内存延迟。结果测出来一片混乱,因为计时函数本身的成本已经超过了被测对象的延迟。解决办法就是用 TSC,并且确认 TSC 时钟源稳定。
第二个误区:测 SSD 时忘开 O_DIRECT。文件系统缓存会把所有读请求都拦截在内存里,你测出来的不是 SSD 速度,而是页缓存速度,误差高达 1000 倍。判断方法很简单:如果连续读同一个文件多次,每次耗时都很稳定且极短,说明命中了缓存。
第三个误区:网络测试时没注意并发影响。如果你同时开很多下载任务或浏览器,网络的排队延迟会显著上升,测出来的数字不是真实链路水平。正确做法是关掉其他联网程序,并且多测几次取中位数,避开网络尖峰。
还有一个很隐蔽的问题:CPU 降频。笔记本插电和不插电,性能差异很大。Windows 下要设置"最佳性能"电源模式,Linux 下可以临时把 CPU 调频策略设为 performance:
bash复制cpupower frequency-set -g performance
不然你测出的数字会在不同频率之间跳动,换算的时候对不上。
6.4 如果不想写代码,用现成工具替代
有的读者可能对编译代码望而却步。没关系,现成工具也能测出八九不离十的结果:
- 内存延迟:
lmbench里的lat_mem_rd命令,能测不同容量下的内存延迟曲线。 - SSD 随机读延迟:
fio按前面说的命令跑。 - 网络延迟:
ping看 RTT,或者用iperf3看吞吐。但iperf3测的是吞吐,不是单次延迟,两个指标需要分开理解。 - HTTPS 请求延迟:curl 的
-w参数。 - 磁盘健康与基准测试:Windows 上可以用 HD Tune 或 Victoria 这类图形化工具,它们不仅能测速度,还能看健康状态。不过这类工具测的是磁盘工具的视角,和 CPU 周期视角略有区别——它们的计时精度通常是微秒级,够用,但测不了纳秒级的内存访问。
工具用归用,我还是建议至少亲手跑一次 C 代码的 TSC 测量,因为只有亲手把 rdtsc 的读数换算成微秒,你才会对"30 纳秒""50 微秒""10 毫秒"这些数字产生真正的肌肉记忆。
最后说点体会
我把这套测量做完之后,最大的改变不是学会了几个命令,而是看问题的角度变了。以前遇到系统卡顿,我会本能地怀疑"是不是代码写得不够高效",现在我会先问:这次操作要跨几个存储层级?要等几次网络 RTT?如果答案是需要等一次机械硬盘随机读,那不管代码怎么写,10 毫秒的下限就在那里。
这种数量级直觉还会让你在做技术选型时少走弯路。比如同事提议在跨地域场景下,通过频繁同步调用来获取最新的配置——你用这张表一算,每次同步 200 毫秒,哪怕每秒只同步一次,也白白吞掉了 CPU 20% 的等待时间。这时候你就会自然地想到改成本地缓存加定期拉取的方案。
最后分享一个我常用的土办法:每接触一个新的存储系统或网络服务,我先跑一遍它的延迟基准,把数据记录在个人笔记里,标上"相当于几次内存访问、几次 SSD 读、几个网络 RTT"。做久了,你的脑海里就会自动浮现出一张延迟地图,遇到任何性能问题,都能快速定位到具体层级。
这套方法同样适用于你身边任何一台机器。你的 CPU 一直在试图告诉你硬盘和网络到底有多慢,只是大部分人从来没有认真去听它说话。
