LabHub
学习 学习路径 课程

计算机组成

指令周期 — 取指、译码、执行

在 LabHub 中继续学习

一句话总结

CPU 是一台不断重复以下工作的机器:从内存中取出一条指令(取指),弄清它的含义(译码),然后真正执行它(执行)。

概念图: 指令和数据存放在同一块内存中 · 用一个寄存器保存下一条待执行指令的地址 · 取指(Fetch) · 译码(Decode)

为什么需要它

程序归根结底是排列在内存中的一组数字。必须有人决定以什么顺序读取这些数字,以及要用它们做什么。早期设计者作出的决定一直沿用至今,其中有两点最为关键。第一,指令和数据存放在同一块内存中(冯·诺依曼结构)。第二,用一个寄存器保存下一条待执行指令的地址(程序计数器)。

得益于这两个决定,CPU 只需遵守一条非常简单的规则:“从程序计数器指向的位置取出指令并执行,再把计数器移到下一条指令。”循环、函数调用和条件分支,归根结底都只是改变这个计数器的值。

它如何工作

一个周期大致按以下顺序进行。

  1. 取指(Fetch) — 从程序计数器指向的地址读取指令,放入指令寄存器。
  2. 译码(Decode) — 查看位模式,判断要执行什么运算以及操作数位于何处。
  3. 执行(Execute) — 算术逻辑单元进行计算、访问内存,或把计数器移到其他位置。

问题是,如果只按顺序完成这些步骤,大部分电路在多数时间里都会闲置。译码器工作时,取指单元就在休息。于是人们设计了流水线。就像把洗衣机和烘干机分开使用一样,在第 1 条指令进行译码时,提前取出第 2 条指令。如果有 5 个阶段,理论吞吐量就能提高到 5 倍。

但这并非没有代价。流水线会产生三类冒险(hazard)。

冒险 情况 应对方式
结构冒险 两个阶段同时需要同一套电路 拆分资源,或延迟其中一方
数据冒险 后一条指令需要前一条指令的结果 使用转发;无法转发时暂停流水线
控制冒险 分支结果尚未确定 进行分支预测;预测错误时清空流水线

第三类尤其昂贵。分支预测一旦失败,已经进入流水线的指令就必须全部丢弃。现代 CPU 的流水线有时超过 15 个阶段,因此每次预测失败都会浪费数十个周期。遍历已排序数组的循环比遍历未排序数组快数倍这一著名现象,原因就在这里:条件具有规律时,预测器更容易猜中。

实际工作中的表现

性能测量中经常会看到 IPC(每周期指令数)这一指标。时钟频率相同的情况下,IPC 高一倍,速度也就快一倍。反过来,如果 IPC 低至 0.3,通常并不是 CPU 无法计算,而是它正在等待某些东西。最常见的等待对象就是内存。

因此,不能把“CPU 使用率 100%”直接理解成“运算单元有 100% 的时间都在计算”。即使核心并非处于空闲状态,也可能因为缓存未命中等原因,在等待内存层次结构时停止执行。不过,因阻塞 I/O 而休眠的任务,其等待时间通常不会计入该进程的 CPU 使用时间,因此必须区分内存延迟与 I/O 等待。

接下来的测验将检查什么

检查你能否自行解释:为什么流水线提高了吞吐量,却没有缩短单条指令的延迟;以及为什么分支预测失败的代价如此高。