指令周期 — 取指、译码、执行
一句话总结
CPU 是一台不断重复以下工作的机器:从内存中取出一条指令(取指),弄清它的含义(译码),然后真正执行它(执行)。
为什么需要它
程序归根结底是排列在内存中的一组数字。必须有人决定以什么顺序读取这些数字,以及要用它们做什么。早期设计者作出的决定一直沿用至今,其中有两点最为关键。第一,指令和数据存放在同一块内存中(冯·诺依曼结构)。第二,用一个寄存器保存下一条待执行指令的地址(程序计数器)。
得益于这两个决定,CPU 只需遵守一条非常简单的规则:“从程序计数器指向的位置取出指令并执行,再把计数器移到下一条指令。”循环、函数调用和条件分支,归根结底都只是改变这个计数器的值。
它如何工作
一个周期大致按以下顺序进行。
- 取指(Fetch) — 从程序计数器指向的地址读取指令,放入指令寄存器。
- 译码(Decode) — 查看位模式,判断要执行什么运算以及操作数位于何处。
- 执行(Execute) — 算术逻辑单元进行计算、访问内存,或把计数器移到其他位置。
问题是,如果只按顺序完成这些步骤,大部分电路在多数时间里都会闲置。译码器工作时,取指单元就在休息。于是人们设计了流水线。就像把洗衣机和烘干机分开使用一样,在第 1 条指令进行译码时,提前取出第 2 条指令。如果有 5 个阶段,理论吞吐量就能提高到 5 倍。
但这并非没有代价。流水线会产生三类冒险(hazard)。
| 冒险 | 情况 | 应对方式 |
|---|---|---|
| 结构冒险 | 两个阶段同时需要同一套电路 | 拆分资源,或延迟其中一方 |
| 数据冒险 | 后一条指令需要前一条指令的结果 | 使用转发;无法转发时暂停流水线 |
| 控制冒险 | 分支结果尚未确定 | 进行分支预测;预测错误时清空流水线 |
第三类尤其昂贵。分支预测一旦失败,已经进入流水线的指令就必须全部丢弃。现代 CPU 的流水线有时超过 15 个阶段,因此每次预测失败都会浪费数十个周期。遍历已排序数组的循环比遍历未排序数组快数倍这一著名现象,原因就在这里:条件具有规律时,预测器更容易猜中。
实际工作中的表现
性能测量中经常会看到 IPC(每周期指令数)这一指标。时钟频率相同的情况下,IPC 高一倍,速度也就快一倍。反过来,如果 IPC 低至 0.3,通常并不是 CPU 无法计算,而是它正在等待某些东西。最常见的等待对象就是内存。
因此,不能把“CPU 使用率 100%”直接理解成“运算单元有 100% 的时间都在计算”。即使核心并非处于空闲状态,也可能因为缓存未命中等原因,在等待内存层次结构时停止执行。不过,因阻塞 I/O 而休眠的任务,其等待时间通常不会计入该进程的 CPU 使用时间,因此必须区分内存延迟与 I/O 等待。
接下来的测验将检查什么
检查你能否自行解释:为什么流水线提高了吞吐量,却没有缩短单条指令的延迟;以及为什么分支预测失败的代价如此高。