CPU 到底是怎么把程序跑起来的,很多人能背出“取指、译码、执行”六个字,但真到 CPU 跑满、缓存未命中、多核调度、天梯图选购的时候,又会开始凭感觉。这篇文章不铺垫背景,直接沿着一条指令从软件到硬件、从启动到完成的主线,把 CPU 底层原理完整串一遍。读完你会得到三样东西:第一,能准确描述 CPU 执行一条指令的全链路;第二,能解释缓存、流水线、分支预测、多核调度为什么会直接影响程序性能;第三,遇到 CPU 占用高、单核卡顿这类问题,知道用哪些命令定位、从哪里切入排查。
这次的内容主线其实是“10 分钟的 CPU 时间轴”:前 2 分钟看代码如何编译成机器指令,第 3~4 分钟看指令周期里的取指和译码,第 5~6 分钟看执行单元和访存路径,第 7~8 分钟看流水线、分支预测怎么把时间抢回来,最后 2 分钟看多核调度和性能观测手段。文章适合正在准备系统底层面试的后端开发,适合天天和进程线程打交道的客户端工程师,也适合想看懂手机上那串 CPU 天梯图参数再决定买什么设备的硬件爱好者。
1. CPU 底层原理全景:10 分钟路线图
先把 CPU 底层原理里最关键的模块列出来。CPU 不是一个黑盒,它内部可以拆成几个职责非常清晰的组件,每个组件只负责一件事情,但组合起来就能完成复杂的程序执行。
| CPU 核心模块 | 主要作用 | 对程序员的直接影响 |
|---|---|---|
| 寄存器组 | 存放指令地址、操作数、计算结果,CPU 内部最快存储单元 | 函数参数、局部变量、返回值都经过寄存器,寄存器不够才压栈 |
| 控制单元 | 负责取指、译码,生成各部件控制信号 | 直接决定一条指令要经过多少个时钟周期 |
| ALU 算术逻辑单元 | 完成加减、与或、移位等运算 | 所有算术表达式的最终落点 |
| 程序计数器 PC/IP | 保存下一条指令的内存地址 | 函数调用、循环、跳转的本质就是修改 PC |
| 缓存 L1/L2/L3 | 在寄存器和内存之间做速度缓冲 | 数据遍历顺序和数据结构设计会影响命中率,差距可能十倍以上 |
| 总线与存储接口 | CPU 与内存、磁盘、外设交换数据的通路 | 带宽决定大规模数据搬运速度 |
这 10 分钟路线图可以细化成一张非常直观的时间轴。
| 时间点 | 观察对象 | 发生的关键事件 |
|---|---|---|
| 0~1 分钟 | 编译器、汇编器、链接器 | C/Java/Go 代码变成二进制机器指令 |
| 1~3 分钟 | 指令周期 | CPU 进入取指、译码、执行、写回的循环 |
| 3~5 分钟 | 存储层次 | 一条 load 指令从 L1、L2、L3 找到数据或一路搜到内存 |
| 5~7 分钟 | 流水线与分支预测 | CPU 不再一条一条顺序执行,而是多条指令并行推进 |
| 7~9 分钟 | 多核与超线程 | 操作系统把线程调度到物理核和逻辑核上 |
| 9~10 分钟 | 性能观测 | 用 perf、top 看到 IPC、缓存未命中、上下文切换 |
为什么要用“10 分钟”来做比喻?因为 CPU 底层原理的主线只有一条链路:指令从内存进入 CPU,经过译码后交给执行单元,执行结果再写回寄存器或内存。其他所有概念,比如流水线、分支预测、乱序执行、超标量、超线程,本质都是为了让这条链路走得更快而加上的优化手段。先把主线看明白,后面所有东西都是可推导的。
2. 一行 C 代码如何变成 CPU 指令:编译、链接与可执行文件
CPU 不理解int add(int a, int b)这种写法,它只认识二进制机器码。理解 CPU 底层原理的第一个关口,就是搞清楚高级语言和 CPU 之间隔着一层又一层转换。
拿一段最直接的代码举例:
int add(int a, int b) { return a + b; }在 Linux 上先用编译器生成汇编,再用反汇编工具查看机器码:
# 生成汇编文件 gcc -O2 -S add.c -o add.s # 编译成目标文件 gcc -c add.c -o add.o # 反汇编查看机器码 objdump -d add.o反汇编之后你会看到一组接近 CPU 真实语言的指令,x86-64 下大概是这样的形态:
addl %esi, %edi movl %edi, %eax ret这里%edi和%esi就是前两个整数参数所在的寄存器,%eax是返回值寄存器。CPU 拿到的并不是addl这个助记符本身,而是它背后编码成的二进制操作码;addl %esi, %edi在内存里就是几个字节的 0/1 序列。
一条机器指令从结构上可以分为两部分:操作码和操作数。操作码告诉 CPU 做什么,比如加法、减法、跳转还是读取内存;操作数告诉 CPU 操作的数据在哪里,可能是寄存器编号、内存地址,也可能是一个立即数。
这里要区分两个容易混淆的概念:指令集架构和微架构。指令集架构(ISA)规定了指令的编码格式、寄存器数量、寻址方式,是程序员和编译器能看到的那一层;微架构则是 CPU 内部为了执行这些指令而做的具体电路设计。x86、ARM、RISC-V 属于指令集架构,而 Intel/AMD 的某款具体芯片内部怎么做流水线、怎么预测分支,属于微架构。同一个 ISA 可以有完全不同的微架构,这也是为什么 CPU 天梯图上同代产品性能差异很大。
对开发者来说,这一阶段最需要记住的结论是:你的语言运行时、JIT 编译器、宿主程序的性能,最终都会体现在指令数量、指令类型和访存路径上。同样一段 Java 代码,热点方法是否被 JIT 编译成高效指令,差距远大于语言本身的语法差异。
3. 指令周期:CPU 每秒重复几十亿次的固定动作
CPU 底层原理最核心的循环就是指令周期。一条指令从进入到执行完,通常经历取指、译码、执行、访存、写回这几步。现代 CPU 为了提速会把每一步拆得更细,但主干永远是这一条。
// 用 C 语言伪码描述 CPU 的主循环 while (1) { uint32_t instr = fetch(PC); // 取指:从 PC 指向的地址读取指令 PC += instr_len; // 更新程序计数器,指向下一条指令 decode(instr); // 译码:识别操作码和操作数 execute(instr); // 执行:交给 ALU 或访存单元处理 write_back(instr); // 写回:把结果保存到寄存器或内存 }取指阶段,程序计数器(Program Counter,也叫 IP/PC)保存着下一条指令的内存地址。CPU 根据这个地址从指令缓存或内存中把指令字节读回来。指令长度可能是固定的,也可能是变长的,比如 x86 指令长度不等,所以每条指令执行完后 PC 的增量不同;而 ARM、RISC-V 的基础指令不少是定长的,PC 增量也相对固定。
译码阶段是控制单元的主场。它把操作码翻译成一组控制信号,决定 ALU 做哪种运算、哪些寄存器参与、结果写到哪里。这一阶段在硬件上是由译码器电路实现的,本质上是把二进制位模式映射成具体的电路开关信号。
执行阶段由 ALU 完成实际运算。如果是算术指令,ALU 做加减乘除或位运算;如果是访存指令,则进入访存单元去读内存;如果是跳转指令,则修改 PC 的值。可以看到,所谓“CPU 会思考”,实际上只是在执行极其简单的布尔逻辑和算术逻辑,复杂度全部来自指令的组合和数据的规模。
写回阶段把计算结果写进目标寄存器。对程序员来说,这个过程最直观的体现就是函数调用约定:哪个寄存器存返回值、哪些寄存器是调用者保存、哪些是被调用者保存,全部由这个写回阶段的行为来支撑。
最后要说清楚主频和指令周期之间的关系。3GHz 主频意味着 CPU 内部时钟每秒振动 30 亿次,一个时钟周期大约是 0.33 纳秒。一条指令在早期单周期 CPU 上需要固定一个完整周期,在多周期 CPU 上会被拆成多个小周期,在现代流水线 CPU 上则希望每个周期都能完成一条指令的某个阶段。CPU 每秒能执行的指令数,等于主频乘以 IPC(Instructions Per Cycle)。所以只看主频高低没有意义,IPC 同样关键,这就是为什么现代 CPU 用各种手段把 IPC 拉高。
4. 存储器层级与 CPU 的连接:缓存为什么能提速
如果 CPU 每次访问数据都直接去内存,那再高的主频也会被内存延迟拖死。内存访问延迟通常在几十纳秒量级,而 CPU 寄存器访问在亚纳秒量级,两者差距接近两个数量级。为了填平这个鸿沟,CPU 和内存之间加了多层缓存,这就是 CPU 底层原理里著名的存储层次结构。
| 存储层级 | 典型容量规模 | 延迟量级 | 访问特征 |
|---|---|---|---|
| 寄存器 | 几十到几百字节 | < 1 ns | 由指令直接指定,无地址翻译 |
| L1 缓存 | 32KB~64KB 左右 | 约 1 ns | 离核心最近,分指令缓存和数据缓存 |
| L2 缓存 | 几百 KB 到几 MB | 几 ns | 核心私有或小范围共享 |
| L3 缓存 | 几 MB 到几十 MB | 十几 ns 到几十 ns | 多个核心共享 |
| 内存 DRAM | 8GB~64GB | 50~100ns 级别 | 需要通过内存控制器访问 |
| SSD/磁盘 | 数百 GB 到数 TB | 微秒到毫秒级 | 由操作系统管理,不属于 CPU 直接管理 |
缓存能起作用,依赖两个程序行为特征:时间局部性和空间局部性。时间局部性是说一个内存地址被访问后,短时间内很可能再次被访问,典型场景是循环变量和热点数据;空间局部性是说一个地址被访问后,它附近的地址也很快会被访问,典型场景是数组连续遍历。
缓存和内存交换数据的基本单位是缓存行。x86 架构下常见的缓存行是 64 字节,也就是说 CPU 从内存读一个 int 时,实际上会把它周围 64 字节一起搬进缓存。这个设计让“连续访问数组”的代码非常占便宜。下面这段 C 代码能直观看到局部性带来的性能差异:
#include <stdio.h> #include <stdlib.h> #include <time.h> #define N 8192 int main(void) { int *matrix = (int*)malloc(N * N * sizeof(int)); volatile long long sum = 0; clock_t start = clock(); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { sum += matrix[i * N + j]; // 行优先,连续内存 } } printf("行优先遍历: %.3f s\n", (double)(clock() - start) / CLOCKS_PER_SEC); free(matrix); return 0; }把内层循环改成matrix[j * N + i],就是按列跳着访问。每次读一个元素都要把整条缓存行拉进缓存,但下一跳又跳到别的缓存行,L1/L2 命中率会明显下降,运行时间可能从几十毫秒涨到几百毫秒甚至更多。CPU 底层原理里,数据布局往往比算法复杂度更早成为瓶颈。
对于多核 CPU,还有缓存一致性问题。L1/L2 缓存是每个核心各自的,两个核心可能同时缓存了同一块内存数据,某一边改了值,另一边必须及时看到。x86 用的 MESI 协议就是用来维护这种缓存一致性的:缓存行会在 Modified、Exclusive、Shared、Invalid 四个状态之间切换。这个机制从硬件上保证多线程程序看到的是同一份内存视图,但代价是核心之间需要同步,一旦频繁跨核共享数据,性能就会受损。
5. 流水线、分支预测与乱序执行:CPU 如何把时间抢回来
如果不做任何优化,CPU 每执行一条指令都要先取指、再译码、再执行、再写回,完全串行。这种设计实现简单,但同一个时钟周期里,ALU 忙着的时候取指单元闲着,取指单元忙的时候写回阶段又闲着,硬件利用率很低。
CPU 的解决方案是流水线。经典的 MIPS 五级流水线把执行过程拆成取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)五段。理想情况下,第 1 个周期取指单元在取第 1 条指令时,译码单元可以在处理第 0 条指令;第 2 个周期,取指单元取第 2 条,译码单元处理第 1 条,执行单元处理第 0 条。理想流水线能做到每周期完成一条指令,让 IPC 趋近于 1,这就是为什么现代 CPU 设计里流水线已经深到 20 级甚至更多。
但流水线会遇到三类冒险。
第一类是结构冒险,也就是多个指令阶段同时要用同一个硬件资源。比如指令缓存和数据缓存如果共用同一个端口,取指和访存就会打架。解决方法是把指令缓存和数据缓存分离,或者让流水线停顿。
第二类是数据冒险,也是开发者最容易感知的一类。看这个汇编片段:
addl $1, %eax addl %eax, %ebx第二条指令需要第一条指令算出来的%eax结果,但执行到第二条的译码阶段时,第一条可能还没写回。硬件处理手段是数据转发,即把第一条的执行结果直接旁路到第二条的执行单元,不必等写回;如果转发也来不及,就插入停顿周期。编译器层面则通过指令调度来重排没有依赖关系的指令,尽量减少这种等待。
第三类是控制冒险,来源是分支。CPU 在执行if、for、循环跳转时,必须知道分支往哪边走才能继续取指。如果等条件算完再取,流水线会空转很长时间。所以现代 CPU 会做分支预测:预先猜一个方向继续取指,猜对了流水线满速运行,猜错了就要清空已经预测执行的指令,重新回到正确路径,这个代价叫分支预测失败惩罚。
# 用 perf 观察程序的分支行为 perf stat -e cycles,instructions,branch-misses ./your_programbranch-misses 高意味着程序里有很多难以预测的分支,CPU 频繁跳错、频繁清空流水线。对性能敏感的场景,比如核心循环里不要写随机分支,改用查表或数学计算,往往比硬缩代码更有效。
再往后是乱序执行。现代 CPU 内部有一个重排序缓冲区(ROB),指令取回来后先在保留站里等待,只要操作数准备好了就可以跳过前面的指令提前执行,执行结果按原始指令顺序写回。这样单条流水线即使被某条慢指令卡住,后面不依赖它的指令也能继续推进。乱序执行让 CPU 在“单核单线程”的条件下也能程序化地挖掘指令级并行,这是 IPC 能超过 1 的关键,也是为什么超标量 CPU 每个周期可以同时发射多条指令。
6. 多核、超线程与操作系统的智能调度
单核性能再强,也有物理上限,于是 CPU 开始往多核发展。多核 CPU 上有多个物理核心,每个核心都有完整的寄存器组、ALU 和私有的 L1/L2 缓存,共享 L3 缓存和内存控制器。
超线程技术则更进一步,让一个物理核心对外表现出两个逻辑核心。超线程的核心思想不是复制整个执行单元,而是在一个物理核里放两套寄存器状态和两个 PC。当第一个逻辑线程在等内存时不占用 ALU,第二个逻辑线程就可以利用空闲的执行单元继续计算。对操作系统来说,这两个逻辑核像是两个独立 CPU,但实际上它们共享同一个核心的运算资源和缓存带宽。所以超线程对计算密集任务帮助有限,对访存密集和延迟敏感任务效果更明显。
多核之下还有一个绕不开的问题:内存访问不均衡。在 NUMA(非均匀内存访问)架构里,每个 CPU 都可以访问所有内存,但访问自己附近内存比访问其他 CPU 挂载的内存更快。程序如果频繁跨 NUMA 节点访问内存,缓存一致性和内存带宽都会吃亏。服务器场景调优时会用numactl固定内存分配和 CPU 亲和性。
操作系统在多核上的调度也属于 CPU 底层原理的重要延伸。现代 Linux 调度器会尽量把一个线程稳定放在同一个核心上,避免频繁迁移导致缓存失效;大小核架构的手机和笔记本 CPU 则会出现高性能核和高能效核的智能调度,前台交互任务往大核放,后台任务往小核放。这类调度策略不是硬件固化的,而是操作系统内核在管理。
本地开发和服务端运维经常需要用命令确认 CPU 拓扑和绑定关系:
# 查看 CPU 型号、核心数、逻辑核数 lscpu # 查看逻辑核数量 nproc # 查看每个逻辑核对应的物理核信息 cat /proc/cpuinfo | grep -E "processor|model name|physical id|core id" # 把进程绑定到指定的 CPU 核心 taskset -c 0,1 ./your_app把自己改造成“CPU 可感知”的程序,在最极端的性能场景里是有价值的。比如批量任务队列,如果让每个 worker 线程固定到指定物理核,线程只在自己的核上运行,避免系统调度把线程踢来踢去,缓存命中率和任务稳定性都会更好。
关于虚拟机里常见的 vCPU 概念,也可以在这里明确:vCPU 并不是一个物理实体,而是虚拟机软件虚拟出来的处理器资源。分配 2 个 vCPU 通常意味着虚拟机能同时使用宿主机上的 2 个逻辑处理器;超线程开启时,1 个物理核能提供 2 个逻辑处理器,但 vCPU 和物理核的实际换算比例会随虚拟化平台、业务负载和调度策略不同而变化,没有普适的固定公式,云厂商的“几核几 G”规格也只是对逻辑资源的抽象。
7. 指令集架构速写:x86、ARM、RISC-V 与 CPU 天梯图怎么读
CPU 底层原理里还有一个绕不开的概念:指令集架构。x86 是典型 CISC(复杂指令集),指令长度可变、指令种类多、有很强历史兼容性,PC 和服务器长时间以来都依赖这套体系。ARM 则是典型 RISC 路线,基础指令定长、精简,低功耗设计做得好,所以手机 CPU 天梯图上的主流芯片几乎都是 ARM 架构。RISC-V 是新一代开源指令集,生态正在发展,很多嵌入式教育和 AI 芯片都开始围绕它做定制。
指令集架构不同,最直接的感受是在编译层面。同一个 C 程序交叉编译到 x86 和 ARM 上,二进制指令完全不同,性能表现也不同。x86 方便在有限指令条数里完成复合操作,但译码复杂度高;ARM 指令数目更多,但译码简单、功耗和面积更可控。这就是为什么同样的应用在手机上不着火,在高性能 PC 上却能拉满功耗墙。
对普通用户来说,看 CPU 天梯图最容易犯的错就是只盯“主频”。主频只是单位时间内的时钟周期数,实际的执行效率还要看 IPC、缓存大小、内存通道、指令集扩展。更合理的方式是把自己的需求先定下来,再看对应参数。
| 使用场景 | 优先关注参数 | 原因 |
|---|---|---|
| 游戏和桌面响应 | 单核 IPC、主频、L3 缓存 | 很多游戏对单线程延迟敏感 |
| 代码编译、视频渲染 | 核心数、多线程能力、内存带宽 | 这类任务能高效吃满多核 |
| 服务器高并发 | 核心数、超线程、缓存一致性、ECC | 线程上下文切换和内存隔离更关键 |
| 笔记本长期移动办公 | TDP、能效核、大小核调度 | 散热和续航优先级高 |
| 嵌入式/边缘设备 | ISA、功耗、扩展性 | 需要在特定功耗预算内完成计算 |
手机 CPU 天梯图通常还会给出大小核组合、GPU 型号、基带和制程工艺,这些参数对整机功耗影响很大。选 CPU 时不要迷信“数字越大越好”,更靠谱的做法是:先看架构代号,再看同架构下的频率区间、缓存差异,然后结合评测确认功耗释放是否跟得上标称值。CPU 底层原理学完之后再看天梯图,你会明白那些分数背后比的是实际执行速度,而不是纸上参数。
8. CPU 底层原理的落地应用:性能观测与问题排查
CPU 底层原理不是只在简历上起作用,它最直接的落地场景是性能观测和故障排查。拿到一台实验机器,第一步永远是用命令把 CPU 状态摸清。
先看系统整体负载,再定位进程和线程:
# 查看负载和 CPU 使用率 top # 更细致的交互界面 htop # 系统最近1分钟/5分钟/15分钟平均负载 uptime但 CPU 使用率不是唯一指标。真正想要定位 CPU 是否“高效干活”,要看指令执行质量和缓存表现。perf可以把程序执行阶段的 CPU 事件拆开:
perf stat -e cycles,instructions,cache-misses,branch-misses ./your_program这段命令会输出几个关键数字:cycles 是总周期数,instructions 是实际执行的指令数,两者相除就是 IPC;cache-misses 高说明数据布局有问题,branch-misses 高说明分支不可预测。先看 IPC 再看缓存未命中,是 CPU 性能调优的标准入场动作。
日常开发里最容易遇到的是“CPU 跑满但不知道代码卡在哪”。比如 IDEA 里写代码经常卡顿,CPU 飙到几百倍,很多人第一反应是换电脑,其实应该先分成几步排查:首先用任务管理器看是单核 100% 还是整体 100%。如果是单核打满,大概率有一个热点线程或 GC 线程在做重活;如果是整体打满,说明有大量并行任务在同时跑,可能是 JVM 堆内存太小导致频繁 GC,也可能是某个库在疯狂重算。
Java 进程定位线程栈的基本路径:
# 找出 Java 进程 PID jps -l # 打印进程里所有线程的 CPU 占用率 top -Hp <PID> # 抓取线程栈,观察热点线程执行栈 jstack <PID>Windows 环境下可以用 PowerShell 或命令行快速查 CPU 基本信息:
wmic cpu get caption,NumberOfCores,NumberOfLogicalProcessors这里的 CPU 底层原理体现在一个点上:线程上下文切换本身是有代价的。线程数量超过逻辑核心数时,内核就要不断换入换出寄存器状态、刷新缓存,CPU 花在“调度”上的时间变多,花在“执行”上的时间变少。所以不要盲目开大量线程,线程池大小和 CPU 核心数、任务类型是强相关的。
CPU 温度查看则属于另一个层面的观测。台式机和部分笔记本可以通过 BIOS 或第三方监控软件读取 CPU 封装温度传感器,Linux 上可以用lm-sensors读取,Windows 上可以使用主板的监控工具或通用硬件监控软件。温度一旦撞到功耗墙,CPU 会主动降频,这时 CPU 占用率不高,但性能明显下降,排查起来最容易误判成软件问题。
CPU 问题排查可以快速对照:
| 现象 | 可能原因 | 排查切入点 |
|---|---|---|
| 单核 100%,其他核心空闲 | 单线程热点、GC 线程、某库阻塞 | 抓线程栈,找热点线程 |
| 整体 CPU 100%,系统卡顿 | 并行线程过多、死循环、高频率任务 | 降并发、查 runnable 线程 |
| 程序不卡但吞吐极低 | cache-misses 高 | 改数据布局,连续访问内存 |
| 线程数很多但 CPU 空转 | 高上下文切换 | 锁竞争、线程池限流、异步化 |
| 占用不高但运行变慢 | 温度撞墙降频 | 检查功耗和温度监控 |
9. CPU 底层原理如何影响你的代码:从数组遍历到并发伪共享
CPU 底层原理对普通开发者的价值,最终要落到代码设计上。这里说三个最常见的影响点。
第一个是数据结构与缓存局部性。数组遍历为什么比链表遍历稳定,不是因为数组“更高级”,而是因为数组元素在内存中连续排列,CPU 拉进一条缓存行时能连续命中多个元素;链表节点散落在堆上,每访问一个节点都可能触发一次缓存未命中,虽然算法复杂度都是 O(n),实际耗时差距可能达到几倍。HashMap 选择“散列数组 + 链表/红黑树”作为存储结构,数组部分天然具备缓存友好性;当哈希冲突严重导致链表变长,节点指针追踪增加,性能下降也从 CPU 底层原理上说得通。
第二个是并发编程中的伪共享。两个线程修改两个不同的变量,如果这两个变量恰好落在同一条缓存行里,缓存一致性协议会让它们互相冲突。线程 A 修改缓存行 X,线程 B 手里的同一条缓存行会被标记失效,B 再修改时又反过来让 A 失效。结果就是两个线程明明没有共享任何数据,却被迫反复同步缓存。避免方法是在两个变量之间填充 padding,让它们落到不同的缓存行里:
#include <pthread.h> #include <stdatomic.h> struct counter { _Atomic long a; _Atomic long b; }; void *inc_a(void *arg) { for (int i = 0; i < 100000000; i++) { atomic_fetch_add(&c.a, 1); } return NULL; } void *inc_b(void *arg) { for (int i = 0; i < 100000000; i++) { atomic_fetch_add(&c.b, 1); } return NULL; }如果struct counter里只放着 a 和 b,两个线程大概率会争抢同一条缓存行;在 a 和 b 之间插一段 64 字节的填充,让a独占一条缓存线、b独占另一条,程序吞吐量可能会有既直观又明显的提升。这个优化在日志框架、无锁队列、统计计数模块里非常常见。
第三个是异步与协程的本质。generator、async/await 这类语法,表面上是语言特性,底层其实涉及状态机的保存和还原。协程切换时,需要保存当前函数的局部变量、寄存器状态和执行位置;async/await 把一次 IO 等待交给系统,让当前线程不需要阻塞在等待上,因而能用同样的 CPU 资源处理更多并发任务。从 CPU 视角看,异步化降低的是线程上下文切换次数和线程栈占用的内存,而不是减少计算总量。
最后回到最开始的 10 分钟:一条指令从内存取进来、经过译码识别、交给执行单元运算、再写回结果,这个循环每秒发生几十亿次。缓存让这个循环尽量少去慢速内存,流水线和分支预测让它尽量不停顿,多核和超线程让它可以同时处理多路任务,操作系统调度则在更上层决定哪个任务优先享用 CPU。CPU 底层原理并不复杂,难的是把这些原理逐个映射到真实的性能和故障现象里。建议读者先跑一遍perf stat看看自己的程序 IPC 和 cache-misses,再测一下数组行优先与列优先的耗时差异,这两步做完,你对 CPU 的理解会和背“取指-译码-执行”完全不同。