CPU 与指令执行
本文从冯·诺依曼架构出发,逐层深入讲解 CPU 的工作原理:指令集设计、流水线执行模型、三大冒险及解决方案、分支预测、超标量与乱序执行,最终落脚到性能公式与软件开发的关联。读完能理解"为什么 JVM 的循环嵌套顺序会影响性能"这类问题的底层根因。
目录
| 章节 | 说明 |
|---|---|
| 冯·诺依曼架构 | 五大组成部件与存储程序思想 |
| 指令集:CISC vs RISC | 两种设计哲学与性能公式 |
| CPU 流水线 | 五级流水原理与吞吐率提升 |
| 流水线三大冒险 | 结构/数据/控制冒险及解决方案 |
| 分支预测 | 静态预测与动态预测算法 |
| 超标量与乱序执行 | 指令级并行的极致优化 |
| SIMD 指令 | 数据级并行与向量化 |
| CPU 性能公式 | 三要素与调优方向 |
冯·诺依曼架构
冯·诺依曼(Von Neumann)在 1945 年的 First Draft of a Report on the EDVAC 中提出了存储程序计算机模型,确立了现代计算机的基础架构。
五大组成部件
| 部件 | 现代对应 | 职责 |
|---|---|---|
| 运算器(ALU) | CPU 核心 | 算术与逻辑运算 |
| 控制器(CU) | CPU 核心 | 指令调度与程序流控制 |
| 存储器 | 内存 + 磁盘 | 存放程序和数据 |
| 输入设备 | 键盘、网卡等 | 向计算机输入信息 |
| 输出设备 | 显示器、网卡等 | 从计算机输出信息 |
存储程序思想的两个核心
- 可编程:程序不固化在电路里,可以动态加载不同程序
- 存储:程序存放在内存中,可以反复读取执行
任何一台计算机,无论是 PC、服务器还是手机(SoC),都遵循冯·诺依曼体系结构。
指令集:CISC vs RISC
历史背景
早期计算机内存极其昂贵,指令集设计倾向于用可变长度指令压缩存储空间,复杂功能直接用硬件电路实现,这就是 CISC(复杂指令集) 的起源。
1970 年代末,UC Berkeley 的 David Patterson 教授发现:实际程序运行中,80% 的时间使用 20% 的简单指令,由此提出了 RISC(精简指令集) 理念。
对比
| 维度 | CISC(如 x86) | RISC(如 ARM、MIPS) |
|---|---|---|
| 指令长度 | 可变长度 | 固定长度(如 32 位) |
| 指令数量 | 多(数百到数千条) | 少(数十到数百条) |
| 单指令复杂度 | 高,可完成复杂操作 | 低,只做简单操作 |
| 优化目标 | 减少指令数 | 减少 CPI |
| 通用寄存器 | 较少 | 较多(空出晶体管) |
| 代表 | Intel x86/x86-64 | ARM、RISC-V、MIPS |
Intel 微指令架构:CISC 与 RISC 的融合
从 Pentium Pro 开始,Intel 引入了微指令(Micro-Ops)架构:
CISC 机器码
↓ 指令译码器(适配器)
RISC 风格微指令(固定长度)
↓ 微指令缓冲区
超标量乱序执行流水线
- 对外保持 x86 指令集兼容性
- 内部以 RISC 风格执行,享受流水线红利
- L0 Cache 缓存译码结果,减少重复译码开销
现代 Intel/AMD CPU 已不是纯粹的 CISC,而是 CISC+RISC 融合体。ARM 能战胜 Intel 进入移动市场,核心原因是功耗优先设计(ARM A8 单核 2W vs Intel i7 130W),而非 RISC 架构本身。
CPU 流水线
单指令周期处理器的问题
最朴素的设计是单指令周期处理器:每条指令在一个时钟周期内完成。但这要求时钟周期 = 最慢指令的执行时间,导致简单指令也要等待最长时间,主频无法提高。
五级流水线
现代 CPU 把指令执行拆分成独立阶段,让多条指令的不同阶段并行执行:
时钟周期: 1 2 3 4 5 6 7
指令 1: IF ID EX MEM WB
指令 2: IF ID EX MEM WB
指令 3: IF ID EX MEM WB
指令 4: IF ID EX MEM WB
指令 5: IF ID EX MEM WB
| 阶段 | 英文 | 职责 |
|---|---|---|
| 取指令 | IF (Instruction Fetch) | 从内存/Cache 读取指令 |
| 指令译码 | ID (Instruction Decode) | 解析操作码、读取寄存器 |
| 执行 | EX (Execute) | ALU 运算 |
| 访存 | MEM (Memory Access) | 读写内存/Cache |
| 写回 | WB (Write Back) | 结果写入寄存器 |
核心收益:虽然单条指令的延迟不变(仍需 5 个时钟周期),但 CPU 的吞吐率提升了,稳定状态下每个时钟周期完成一条指令。
流水线深度的代价
流水线每增加一级,就要多一次写入流水线寄存器的 overhead(约 20ps)。级数越深,overhead 占比越大。现代 ARM/Intel CPU 流水线通常在 14~20 级。
奔腾 4 的 NetBurst 架构将流水线做到 31 级,追求极高主频,但分支预测失败惩罚极大,最终败于 Core 架构。
流水线三大冒险
结构冒险(Structural Hazard)
本质:多条指令在同一时钟周期争用同一硬件资源。
典型案例:第 1 条指令处于 MEM 阶段(访问数据内存),第 4 条指令处于 IF 阶段(取指令,也要访问内存)—— 两者同时需要内存总线。
解决方案:借鉴哈佛架构,将 L1 Cache 拆分为指令缓存(I-Cache) 和数据缓存(D-Cache),使取指和访存可以并行,互不干扰。
数据冒险(Data Hazard)
本质:后续指令依赖前面指令尚未写回的计算结果。
三种依赖关系:
| 依赖类型 | 英文 | 说明 |
|---|---|---|
| 数据依赖 | RAW (Read After Write) | 先写后读,最常见 |
| 反依赖 | WAR (Write After Read) | 先读后写 |
| 输出依赖 | WAW (Write After Write) | 写后再写 |
解决方案:
- 流水线停顿(Pipeline Stall / Bubble):插入 NOP 指令等待,简单但牺牲性能
- 操作数前推(Operand Forwarding):在硬件上增加旁路,把 EX 阶段的计算结果直接转发给下一条指令的 EX 阶段输入,无需等待写回
- 乱序执行(Out-of-Order Execution):调度器找到没有依赖关系的后续指令提前执行
控制冒险(Control Hazard)
本质:遇到条件跳转指令(if/else、循环)时,CPU 不知道下一条该执行哪条指令。
解决方案:
| 方案 | 说明 | 特点 |
|---|---|---|
| 流水线停顿 | 等待跳转结果确定再取指 | 正确但代价高 |
| 缩短分支延迟 | 把条件判断提前到 ID 阶段 | 减少等待周期数 |
| 静态分支预测 | "假装分支不发生",预测顺序执行 | 简单,约 50% 准确率 |
| 动态分支预测 | 根据历史跳转记录预测 | 准确率可达 93%+ |
分支预测
静态预测
最简单的策略:假装分支不发生,即始终预测顺序执行。预测失败时,将流水线中已执行到一半的指令 Flush(清除),重新取指。
动态分支预测
1 比特饱和计数(一级分支预测):用 1 bit 记录上次跳转结果,直接用上次结果预测下次。
2 比特饱和计数(双模态预测器):用 4 状态机,需连续两次"反转"才改变预测方向,更稳定:
强不跳转 ←→ 弱不跳转 ←→ 弱跳转 ←→ 强跳转
2 比特预测器在 SPEC 89 测试中准确率达 93.5%,Intel Pentium 时代使用此方案。
软件开发影响:循环嵌套顺序
// 方案 A:内层循环 10000 次
for (int i = 0; i < 100; i++)
for (int j = 0; j < 1000; j++)
for (int k = 0; k < 10000; k++) { }
// 分支预测失败次数:100 × 1000 = 10 万次
// 方案 B:内层循环 100 次
for (int i = 0; i < 10000; i++)
for (int j = 0; j < 1000; j++)
for (int k = 0; k < 100; k++) { }
// 分支预测失败次数:10000 × 1000 = 1000 万次
实测方案 B 比方案 A 慢约 3 倍。内层循环次数越多,分支预测失败越少,性能越好。
超标量与乱序执行
超标量(Superscalar)
单个时钟周期内同时发射多条指令到不同的执行单元(ALU、FPU、Load/Store 等),使 CPI < 1。
时钟周期 N: 指令 1 → ALU1
指令 2 → ALU2
指令 3 → FPU
现代 CPU 通常是 4~6 路超标量。
乱序执行(Out-of-Order Execution)
CPU 内部维护一个指令调度队列(Reservation Station),找出没有数据依赖的指令提前执行,打破程序顺序约束。结果通过重排序缓冲区(ROB) 按原始顺序提交,对外表现仍是顺序执行。
乱序执行是 CPU 自动完成的,对程序员透明。但在多核场景下,需要通过内存屏障(Memory Barrier)来约束编译器和 CPU 的重排序行为。
SIMD 指令
SIMD(Single Instruction Multiple Data):一条指令同时操作多个数据,实现数据级并行。
标量加法: a[0]+b[0], a[1]+b[1], a[2]+b[2], a[3]+b[3] → 4 条指令
SIMD 加法: [a[0],a[1],a[2],a[3]] + [b[0],b[1],b[2],b[3]] → 1 条指令
| 指令集 | 寄存器宽度 | 适用场景 |
|---|---|---|
| SSE/SSE2 | 128 bit | 早期多媒体处理 |
| AVX/AVX2 | 256 bit | 图像处理、科学计算 |
| AVX-512 | 512 bit | 深度学习推理 |
开发关联:Java 从 JDK 16 开始通过 Vector API 暴露 SIMD 能力;JVM JIT 编译器也会自动对某些循环进行向量化优化。
CPU 性能公式
$$\text{程序执行时间} = \text{指令数} \times \text{CPI} \times \text{时钟周期时间}$$
| 要素 | 含义 | 优化方向 |
|---|---|---|
| 指令数 | 程序执行的总指令条数 | 编译器优化、算法优化、CISC 减少指令数 |
| CPI | 每条指令平均时钟周期数 | 流水线、超标量、乱序执行、减少 Cache Miss |
| 时钟周期时间 | 1 / 主频 | 提升主频(受功耗墙限制) |
功耗墙(Power Wall):主频提升导致功耗以立方级增长,散热无法解决,这是为什么 2004 年后单核主频停止大幅提升,转向多核路线的根本原因。
参考资料
- 《深入浅出计算机组成原理》— 极客时间,郑晔
- 《计算机组成与设计:硬件/软件接口》— Patterson & Hennessy,第 4 章
- 《深入理解计算机系统》(CSAPP)— 第 4 章
评论 (0)
发表评论