2. 计算机体系结构基础
说实话,很多做性能优化的朋友,一上来就盯着代码看。我当年也是这样。直到有一次线上服务CPU跑满,我翻来覆去查代码逻辑,最后发现是缓存行伪共享的问题。那一刻我才明白——不懂底层体系结构,优化就是瞎蒙。
这一章,咱们就把计算机体系结构的三个核心模块掰开揉碎。你搞懂了这些,后面做调优才有底气。
CPU架构与性能
CPU是计算机的大脑。但大脑怎么工作,直接影响你的程序跑多快。
指令流水线与超标量
早期的CPU一次只处理一条指令。取指、译码、执行、写回,串着来。后来工程师发现,这太浪费了。于是搞出了流水线——第一条指令在执行,第二条已经在译码,第三条正在取指。就像工厂流水线,每个环节都不闲着。
但流水线有个大坑:分支预测失败。我遇到过一段代码,里面有个if-else分支,概率各50%。CPU猜错了就得清空流水线,白白浪费十几个时钟周期。后来我把分支改成了查表法,性能直接翻倍。
乱序执行与投机执行
CPU为了不闲着,会偷偷调整指令的执行顺序。比如你写了:
a = b + c
d = e + f
g = a + d
CPU发现第二条指令不依赖第一条,就会先执行它。等第一条结果出来了,再算g。这就是乱序执行。
投机执行更激进。遇到分支时,CPU先猜一个方向,提前执行后面的指令。猜对了,皆大欢喜。猜错了,就得回滚。嗯,这里要注意——回滚是有代价的。我见过一个极端案例,分支预测成功率只有60%,性能比100%预测时差了将近3倍。
指令集与微架构
指令集是CPU能理解的语言。x86、ARM、RISC-V,这些都是指令集。微架构是具体怎么实现这些指令。同样的x86指令,Intel和AMD的实现方式完全不同。
我个人习惯在做性能分析时,先搞清楚目标CPU的微架构。比如Intel的Skylake和Ice Lake,L1缓存延迟就差了一个时钟周期。别小看这一个周期,高频场景下差距就出来了。
lscpu 命令查看CPU信息。重点关注缓存大小、NUMA节点、指令集支持。这些数据直接指导你的优化方向。
内存层次结构
CPU跑得飞快,但内存跟不上。这就是冯·诺依曼瓶颈。为了解决这个问题,工程师搞出了多级缓存。
缓存层级与延迟
典型的现代CPU有三级缓存:
| 层级 | 典型大小 | 延迟(时钟周期) | 带宽 |
|---|---|---|---|
| L1 | 32KB - 64KB | 3-5 | 极高 |
| L2 | 256KB - 1MB | 10-15 | 高 |
| L3 | 8MB - 32MB | 30-50 | 中等 |
| 主存 | 8GB - 512GB | 100-300 | 低 |
你看,L1比主存快了将近100倍。所以优化的核心思路就是:让数据尽量待在L1里。
我曾经优化过一个图像处理算法。原始代码用二维数组存像素,按行遍历。改成按列遍历后,性能反而下降了。为什么?因为C语言是行优先存储,按列访问会导致缓存行利用率极低。每次只取一个int,却要浪费64字节的缓存行。
TLB与页表
虚拟内存到物理地址的映射,靠页表。但页表在内存里,每次访问都要查,太慢了。于是CPU搞了个TLB(快表),专门缓存最近用过的页表项。
TLB命中率对性能影响巨大。我见过一个数据库应用,因为使用了大量内存映射文件,TLB频繁失效,性能下降了40%。后来改用大页(2MB或1GB),TLB覆盖范围大了,问题就解决了。
NUMA架构
多路服务器里,每个CPU有自己的本地内存。访问本地内存快,访问远端内存慢。这就是NUMA(非统一内存访问)。
我调优过一个内存密集型应用。默认情况下,内存分配是随机的。结果大量数据落在了远端内存上,延迟高了30%。后来我绑定了CPU亲和性,并强制在本地分配内存,性能就上来了。
# 查看NUMA拓扑
numactl --hardware
# 绑定进程到指定CPU和内存节点
numactl --cpunodebind=0 --membind=0 ./myapp
I/O子系统
I/O是性能优化的重灾区。说白了,CPU和内存之间的速度差已经够大了,I/O设备更慢。
存储层次
从快到慢:寄存器 > L1缓存 > L2缓存 > L3缓存 > 主存 > SSD > 机械硬盘。每一级的速度差都是数量级的。
我优化过一个日志系统。原始方案是每条日志都直接写磁盘。QPS一高,磁盘就扛不住了。后来改成批量写入,攒够4KB再刷盘,性能提升了10倍。你想想看,4KB正好是一个文件系统块的大小,一次I/O操作就能写完。
中断与轮询
传统I/O靠中断。设备准备好了,发个中断通知CPU。但高并发场景下,中断太多,CPU光处理中断就忙不过来了。
现代方案是轮询。比如DPDK和SPDK,让CPU主动去检查设备状态。虽然CPU占用率高,但延迟低、吞吐大。我做过一个网络网关,用DPDK替换内核协议栈后,包处理能力从100万pps提升到了1000万pps。
DMA与零拷贝
传统I/O流程:磁盘 -> 内核缓冲区 -> 用户缓冲区。数据要拷贝两次。DMA让设备直接访问内存,CPU不用参与数据搬运。零拷贝更进一步,让用户程序直接访问设备数据。
我优化过一个视频流服务。原始实现用read/write,每次都要在内核和用户空间之间拷贝数据。改成mmap后,CPU占用率从80%降到了30%。
// 传统方式:两次拷贝
read(fd, buf, size); // 内核 -> 用户
write(sock, buf, size); // 用户 -> 内核
// 零拷贝:一次搞定
sendfile(sock, fd, NULL, size); // 内核直接发送
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你对照着看,思路会更清晰。
这张图把CPU、内存、I/O三个子系统串在了一起。你仔细看底部的延迟数据——从L1到机械硬盘,差了整整一千万倍。这就是为什么性能优化的第一原则是:减少数据移动。
好了,这一章的内容就到这里。记住,体系结构不是纸上谈兵。下次你遇到性能问题,先想想数据在哪个层级,CPU在干什么,I/O是不是瓶颈。带着这些思路去分析,很多问题就迎刃而解了。