真值 ($X$) 与 补码 ($[X]_{补}$) 之间的三大转换规则1. 真值 $\rightarrow$ 补码 ($X \rightarrow [X]_{补}$) 这是最基础的编码 过程。
正数: 符号位设为 0 ,数值部分完全照抄 ,不变。
负数: 符号位设为 1 ,数值部分执行 “各位取反,末位加 1” 。
2. 补码 $\rightarrow$ 真值 ($[X]_{补} \rightarrow X$) 这是解码 过程。PPT 强调了正负数的处理逻辑是对称的:
符号位为 0(正数): 数值部分直接就是真值。
符号位为 1(负数): 数值部分再次执行 “各位取反,末位加 1” 。
点评:这验证了你之前的发现——求负数补码的原码,操作逻辑和求补码是一模一样的!
3. 求相反数的补码 ($[X]{补} \rightarrow [-X] {补}$) 这是数学上的变号 操作(例如从 $5$ 的补码求 $-5$ 的补码)。
规则: 对 $[X]_{补}$ 的所有位 (注意 PPT 写的:符号位参与运算 )执行 “各位取反,末位加 1” 。
💡 核心规律总结 (PPT 右下角方框的含义) PPT 右下角的方框其实在告诉你,以下三个操作用的是同一个 核心算法(取反加一):
$X \rightarrow [-X]_{补}$ :把一个正数真值变成负数补码。
$[X]_{补} \rightarrow X$ :把一个负数补码还原成真值(数值部分)。
$[X]{补} \rightarrow [-X] {补}$ :已知一个数的补码,求它相反数的补码。
一句话总结这张 PPT:
在补码系统中,只要涉及“负数”或者“变号”,核心法宝就是 “各位取反,末位加 1”。
奇偶校验法 为了让你彻底明白,我们用一个最简单的ASCII 字符 传输作为例子。
假设我们要传输字母 “A”。
字母 “A” 的二进制 ASCII 码是:1000001(共 7 位)。
现在,我们要给它加上第 8 位——校验位 。
1. 原始数据分析
数据: 1 0 0 0 0 0 1
数一数“1”的个数: 这里有 2 个 1。
判断: 2 是偶数 。
2. 奇校验 (Odd Parity) 的例子 目标: 加上校验位后,整个串里“1”的总数必须是 奇数 。
现状: 现在有 2 个 1(偶数)。
操作: 既然还差一个 1 才能变成奇数,所以校验位必须填 1 。
最终发送的数据:
1000001 + 1 (校验位) $\rightarrow$ 10000011
验证: 总共有 3 个 1 $\rightarrow$ 奇数 $\rightarrow$ ✅ 合格。
3. 偶校验 (Even Parity) 的例子 目标: 加上校验位后,整个串里“1”的总数必须是 偶数 。
现状: 现在有 2 个 1(已经由偶数了)。
操作: 不需要再加 1 了(否则就变成奇数了),所以校验位填 0 。
最终发送的数据:
1000001 + 0 (校验位) $\rightarrow$ 10000010
验证: 总共有 2 个 1 $\rightarrow$ 偶数 $\rightarrow$ ✅ 合格。
4. 它是怎么发现错误的?(以奇校验为例) 假设我们使用 奇校验 ,发送了 10000011 (3个1,正常)。
场景一:传输正常
接收方收到: 10000011
检查: 数一下有 3 个 1(奇数)。
结论: 校验通过,数据应该是对的。
场景二:发生 1 位错误(比如第1位传丢了,1变成了0)
接收方收到: 00000011 (原本的第一个1变成了0)
检查: 数一下,现在只有 2 个 1(偶数)。
结论: 不对! 我们约定好是奇校验的,怎么变成偶数了?报错!
场景三:发生 2 位错误(这是奇偶校验的死穴)
假设第1位和最后一位都翻转了。
接收方收到: 00000010
检查: 数一下,有 1 个 1(奇数)。
结论: 接收方会以为数据是对的(因为1的个数还是奇数),但实际上数据已经错得离谱了。
总结表格
原始数据 (1的个数)
校验方式
校验位填什么?
最终结果 (数据+校验)
最终1的个数
1000001 (2个, 偶)
奇校验
1
10000011
3 (奇)
1000001 (2个, 偶)
偶校验
0
10000010
2 (偶)
1110000 (3个, 奇)
奇校验
0
11100000
3 (奇)
1110000 (3个, 奇)
偶校验
1
11100001
4 (偶)
海明校验码(略有涉及) 基于我们刚才的深度探讨,我为你整理了一份“海明校验码核心总结” 。这份总结略过了死板的教科书定义,专门针对你刚才产生的核心顿悟 (关于位置、倒序、多位错误等)进行梳理。
你可以把它当作一份复习备忘录:
1. 核心本质:它是一个“坐标定位系统” 海明码不只是简单的加减法,而是利用二进制权重 (1, 2, 4, 8…)构建了一套坐标系。
目的: 不仅告诉你“错了”,还能精准地告诉你“第几位错了”。
2. 生成阶段:如何布阵?
确定人数: 使用公式 $2^r \ge k + r + 1$ 。
你的理解: 必须有足够的校验位(状态数),才能覆盖所有可能出错的位置 + 1种全对的情况。
站位: 校验位必须站在 $2^n$ (1, 2, 4, 8…) 的位置上。
计算: 每一个校验位负责监管“二进制位索引中包含自己权重”的那些数据位(利用偶校验 原则填充 0 或 1)。
3. 校验阶段:如何抓内鬼? 这是你刚才理解最深刻的部分。
操作逻辑: 不区分 数据位和校验位,将收到的所有位 作为一个整体进行计算。
计算校正子(故障码):
重新计算 P1, P2, P4… 各自辖区内 1 的个数。
偶数 $\rightarrow$ 记 0。
奇数 $\rightarrow$ 记 1。
关键步骤: 倒序排列 ($S_4 S_2 S_1$)。
4. 判决阶段:结果意味着什么? 根据算出的“故障码”(二进制数转十进制):
结果 = 0:
结论: 传输成功(或者发生了极小概率的偶数位相互抵消)。
结果 = X (非0):
结论: 第 X 位出错了。
动作: 无论第 X 位是数据还是校验码,直接将其取反 (0变1,1变0),数据即修复。
5. 致命盲区:如果错了 2 位怎么办? 这是我们最后讨论的重点。
普通海明码: 会“撒谎”。两个错误叠加会算出一个错误的坐标 ,导致系统去修改一个无辜的正确位(越改越错)。
解决方案(SECDED): 增加第 8 个“全校验位” 。
如果海明码说有错(故障码非0),但全校验位说没错(整体偶校验正常)。
结论: 发生了双比特错误 。系统会报警拒绝处理,而不是乱修。
一句话心法:
海明码就是把校验位安插在二进制权重点上,接收时重算一遍奇偶性,算出来的二进制数就是出错位置的坐标。
现在的你已经完全具备了手算海明码甚至去写代码实现它的逻辑基础了!需要我给你出一道终极测试题练练手吗?
原码一位乘法
它的核心思想只有一句话:“符号单独算,数值当无符号数算”。
1. 核心步骤 原码一位乘法把整个过程拆成了两个独立的任务:
任务一:处理符号位 (简单)
规则: 同号得正,异号得负。
运算: 用异或门 ($XOR$) 完成。
(例如:$0 \oplus 1 = 1$,结果为负)
任务二:处理数值位 (核心)
规则: 既然符号已经拿走了,剩下的绝对值 $|X|$ 和 $|Y|$ 其实就是两个正整数 (或者说无符号数)。
算法: 直接套用你最开始学的“无符号乘法” (列竖式法)。
看 $Y$ 的最后一位。
是 1 $\rightarrow$ 加 $|X|$。
是 0 $\rightarrow$ 加 0。
逻辑右移 (前面补0,因为是绝对值运算,不涉及负数符号维持)。
2. 举例演示 假设我们要计算 $X \times Y$ :
$X = -0.1101$ (原码 1.1101)
$Y = +0.1011$ (原码 0.1011)
第一步:定符号 把这个 1 存起来,最后贴到结果上。
第二步:算数值 ($1101 \times 1011$) 这部分完全就是无符号乘法 。
被乘数 $|X| = 1101$
乘数 $|Y| = 1011$
部分积 $P$ 初始化为 $0000$
轮次
Y 末位
动作
P (部分积)
Y (乘数)
说明
初始
1
0000
1011
1
1
+X
1101
$0000+1101$
逻辑右移
0110
1101
高位补0!
2
1
+X
10011
$0110+1101=10011$
逻辑右移
1001
1110
进位1移进来
3
0
+0
1001
不加
逻辑右移
0100
1111
高位补0
4
1
+X
10001
$0100+1101=10001$
逻辑右移
1000
1111
进位1移进来
注意:这里的右移全部是逻辑右移 (左边补0),因为我们在算绝对值,绝对值永远是正的。
第三步:拼接结果
数值部分: $P$ 和 $Y$ 拼起来 $\rightarrow$ 1000 1111
加上小数点: 0.10001111
加上符号(第一步算的): 1.10001111
布斯算法 布斯乘法算法 (Booth’s Multiplication Algorithm)是一种用于计算带符号二进制数 (通常使用补码表示)的乘法算法。
相较于你之前了解的“原码一位乘法”(主要处理无符号数或需要单独处理符号位),布斯乘法可以直接对补码进行操作,不需要将符号位和数值位分开计算,这使得硬件电路设计更加统一和高效。
1. 核心原理 布斯乘法通过检查乘数的最后一位 ($Q0$)和辅助位 ($Q {-1}$,初始为0)的状态变化来决定操作。它利用了“连续的1序列可以转换为一次减法和一次加法”的数学性质(例如 $99 = 100 - 1$),从而减少运算次数。
2. 运算规则表 每次检查乘数的最后两位($Q0, Q {-1}$),执行以下操作,然后进行算术右移 :
Q0 (当前位)
Q−1 (辅助位)
操作 (针对部分积 A)
说明
0
0
不操作 ,直接右移
连续的0,无需处理
0
1
+ 被乘数 (M),然后右移
遇到1序列的结束
1
0
- 被乘数 (M),然后右移
遇到1序列的开始
1
1
不操作 ,直接右移
连续的1,中间无需处理
注意:这里的“- 被乘数”通常通过“+ [-M]补”来实现。
3. 计算步骤演示 假设我们用 4位补码计算:$2 \times (-3) = -6$
被乘数 (M) = $2$ $\rightarrow$ 0010
乘数 (Q) = $-3$ $\rightarrow$ 1101
累加器 (A) = 0000 (初始化为0)
辅助位 ($Q_{-1}$) = 0
计数器 = 4 (因为是4位)
演算过程:
步骤
Q0,Q−1
操作
累加器 A
乘数 Q
辅助位 Q−1
说明
初始
0000
1101
0
1
1, 0
A = A - M (即 A + 1110) 0000 + 1110 = 1110
1110
1101
0
减被乘数
算术右移
1111
0110
1
符号位保持不变
2
0, 1
A = A + M (即 A + 0010) 1111 + 0010 = 0001
0001
0110
1
加被乘数
算术右移
0000
1011
0
3
1, 0
A = A - M (即 A + 1110) 0000 + 1110 = 1110
1110
1011
0
减被乘数
算术右移
1111
0101
1
4
1, 1
无操作
1111
0101
1
算术右移
1111
1010
1
最终结果:
将 A 和 Q 拼接起来(不看 $Q_{-1}$):1111 1010。
这是补码形式,转换为十进制:
符号位是1,取反加1得到原码:-(0000 0101 + 1) = - (0000 0110) = -6。
结果正确。
恢复余数法
加减交替法 原码除法 恢复余数法和不恢复余数法(加减交替法) 计组_哔哩哔哩_bilibili
整数除法处理过程_哔哩哔哩_bilibili
整数不恢复余数除法 中,被除数通常要进行位扩展
在不恢复余数除法(加减交替法)\ 中,**整数除法**和*小数除法的核心算法是一样的,都是根据余数的正负决定商位和下一步操作。但它们在 初始设置、精度控制、终止条件、结果处理* 等方面有明显区别。
定点整数的除法 - 知乎
空间局部性和时间局部性 这两个概念是计算机系统(特别是缓存 Cache )能够高效工作的基石。如果程序员写的代码符合这两个特性,程序的运行速度可能会快几十倍甚至上百倍。
简单来说,CPU 跑得飞快,内存(RAM)跑得比较慢。为了不让 CPU 傻等,我们需要猜测 CPU 接下来要用什么数据,并提前把它搬到离 CPU 最近的缓存里。
局部性原理(Principle of Locality) 就是我们用来猜测的“依据”。
1. 时间局部性 (Temporal Locality) 一句话解释: “如果一个数据现在被访问了,那么它在不久的将来很可能再次被访问。”
代码实例 看看下面这个简单的循环:
C
1 2 3 4 int sum = 0; for (int i = 0; i < 1000; i++) { sum += i; }
变量 sum 和 i: 在每一次循环中,这两个变量都被反复读取和修改。
指令本身: 循环体内的机器指令(代码本身)也是被一遍又一遍地执行。
硬件是怎么做的?
因为有时间局部性,CPU 会把 sum 和 i 以及循环内的指令死死地抓在 L1 Cache(一级缓存) 或者 寄存器 里,绝对不会每次都去慢吞吞的内存条里找。
2. 空间局部性 (Spatial Locality) 一句话解释: “如果一个数据被访问了,那么它附近的数据在不久的将来很可能也会被访问。”
代码实例 这是最典型的数组遍历:
C
1 2 3 4 5 6 7 int arr[1000]; // ... 假设数组里有数据 ... for (int i = 0; i < 1000; i++) { // 按顺序访问:arr[0], arr[1], arr[2]... printf("%d", arr[i]); }
数组 arr: 在内存中,数组的元素是连续存放 的。arr[0] 的隔壁就是 arr[1]。
访问模式: 当程序请求 arr[0] 时,CPU 知道你有“空间局部性”的倾向。
硬件是怎么做的?
当 CPU 去内存取 arr[0] 时,内存条并不是只给一个数字,而是直接给一个 “缓存行” (Cache Line)(通常是 64 字节)。
这意味着,虽然你只喊了 arr[0],但 arr[1] 到 arr[15] 可能已经被顺手打包带进了 CPU 的缓存。当你下一秒要用 arr[1] 时,它已经在缓存里等着你了,速度极快。
3. 一个经典的“反面教材” 为了让你感受到空间局部性的威力,我们看一个经典的二维数组遍历 的例子。
假设有一个巨大的二维数组 matrix[10000][10000]。在 C/C++ 语言中,数组是按行存储 的(Row-major),也就是说,第一行的数据在内存里是连在一起的,接着是第二行。
写法 A(利用空间局部性 - 快): 1 2 3 4 5 6 // 按行遍历:外层是行(i),内层是列(j) for (int i = 0; i < 10000; i++) { for (int j = 0; j < 10000; j++) { sum += matrix[i][j]; } }
分析: 访问顺序是 [0][0], [0][1], [0][2]...。这在内存里是连续的。缓存一次加载一行,命中率极高。
写法 B(破坏空间局部性 - 慢): 1 2 3 4 5 6 // 按列遍历:外层是列(j),内层是行(i) for (int j = 0; j < 10000; j++) { for (int i = 0; i < 10000; i++) { sum += matrix[i][j]; } }
分析: 访问顺序是 [0][0], [1][0], [2][0]...。
问题: [0][0] 和 [1][0] 在内存中相隔了整整一行(10000个元素)!
后果: 每次访问都像是“大跳跃”。CPU 加载了 [0][0] 及其附近的缓存行,结果你下一步要的是十万八千里外的 [1][0]。缓存全部失效(Cache Miss) 。这会导致程序运行极其缓慢,甚至慢上几十倍。
cache行和主存块之间的映射方式 1. 直接映射 (Direct Mapping) 口诀: “死板、对号入座”
这是最简单的规则。我们规定:根据车牌号的末尾数字,只能停在固定的车位。
规则公式 举个例子 假设 Cache 只有 4 个车位(0, 1, 2, 3)。
主存有很多块(0, 1, 2, 3, 4, 5…)。
第 0 号主存块: $0 \mod 4 = 0$ $\rightarrow$ 只能停 0号 车位。
第 1 号主存块: $1 \mod 4 = 1$ $\rightarrow$ 只能停 1号 车位。
…
第 4 号主存块: $4 \mod 4 = 0$ $\rightarrow$ 也要停 0号 车位!
第 8 号主存块: $8 \mod 4 = 0$ $\rightarrow$ 还要停 0号 车位!
优缺点
2. 全相联映射 (Fully Associative Mapping) 口诀: “自由、随便停”
这是最灵活的规则。我们规定:只要有空位,想停哪儿停哪儿。
规则 没有公式。主存块可以放在 Cache 的任意 一个行中。
举个例子 还是 4 个车位。
第 0 号块来了 $\rightarrow$ 停在 0 号位。
第 4 号块来了 $\rightarrow$ 0号位有人了?没关系,停在 1 号位。
第 8 号块来了 $\rightarrow$ 停在 2 号位。
优缺点
优点: 空间利用率极高 ,只要Cache没满,就不会发生冲突踢人的情况。
缺点: 找车太慢(太贵)。
当你(CPU)想找“第4号块”时,你不知道它在哪里。你必须同时检查所有的车位(0,1,2,3…)。
这需要非常复杂的硬件电路(并行比较器),如果 Cache 很大,这种电路极其昂贵且耗电。
3. 组相联映射 (Set Associative Mapping) 口诀: “中庸之道、分组管理”
这是现代 CPU(如 Intel Core, AMD Ryzen)普遍采用的方式。它折中了前两者的方案。
它把 Cache 分成了若干个“组” (Set) ,每个组里包含几个车位(比如 2 个或 4 个)。
规则
先定位组(像直接映射): 你必须去指定的组。
再定位置(像全相联): 到了那个组之后,组内的车位随便停 。
这被称为 N-路 组相联 (N-way Set Associative) ,这里的 N 就是一个组里有几个车位。
举个例子(2路组相联) 假设 Cache 还是 4 个车位,但我们将它们分为 2 个组 (Set 0 和 Set 1)。
Set 0 包含:车位0、车位1
Set 1 包含:车位2、车位3
现在我们要停 第 4 号主存块 :
算组号: $4 \mod 2(\text{组数}) = 0$。所以必须去 Set 0 。
找位子: 到了 Set 0,发现里面有“车位0”和“车位1”。只要这两个有一个是空的,第4号块就能停进去。
优缺点
优点:
比直接映射灵活:就算 Set 0 里已经停了“第0号块”,我“第4号块”来了还能停在 Set 0 的另一个空位,不会打架。
比全相联便宜:查找时,只需要在特定的组内(比如搜4个位置)找,不需要全场搜。
地位: 这是工业界的标准答案 。
直接映射中,主存地址的结构
1. 地址结构的宏观样子 假设 CPU 发出的地址是 $N$ 位二进制数(比如 32位)。在直接映射模式下,它被切分为:
每一段都有其特定的使命。
2. 深度拆解:每一段是干嘛的? 为了方便理解,我们设定一个具体的场景:
地址总长度: 32位
Cache 大小: 4 KB ($2^{12}$ 字节)
块大小 (Block Size): 64 Byte ($2^6$ 字节)
根据这个配置,我们来计算每一段的长度和作用。
第一部分:块内偏移 (Block Offset) —— “具体的字节在哪?”
第二部分:行索引 (Cache Line Index) —— “停在哪个车位?”
位置: 地址的中间部分。
作用: 这是直接映射的核心。它决定了这块数据必须 存放在 Cache 的第几行(第几个车位)。
怎么算位数?
看 Cache 一共有多少行。
在本例中:$4 \text{KB} / 64 \text{B} = 4096 / 64 = 64$ 行。
64 行 = $2^6$。
所以,需要 6 位 来定位这 64 个行(000000 到 111111)。
第三部分:标记 (Tag) —— “你是谁?”
直写和回写 1. 直写 (Write-Through) 口诀: “老实人、立刻同步”
原理 每当 CPU 要往 Cache 里写数据时,它会同时把数据写回主存。 即:写 Cache + 写主存 同时进行。
生活比喻: 你每花一笔钱,不仅记在草稿本上,还立刻 跑去老板办公室,把正式账本也更新了。
优缺点
优点(简单可靠):
一致性极好: 主存里的数据永远是最新的。
实现简单: 既然主存总是新的,当 Cache 里的这块数据需要被踢走(替换)时,直接丢弃就行,不需要做任何操作。
缺点(慢):
速度受拖累: CPU 跑得快,但主存太慢了。每次写操作 CPU 都要等主存写完,大大降低了速度。
总线太忙: 哪怕只改了一个字节,都要往主存发一次信号,总线带宽被占满了。
补救措施:写缓冲 (Write Buffer) 为了不让 CPU 傻等主存,通常会在 CPU 和主存之间加一个“写缓冲队列” 。 CPU 把数据扔进缓冲队列就立刻溜走去干别的,由硬件慢慢把队列里的数据写进主存。
2. 回写 (Write-Back) 口诀: “懒人智慧、秋后算账”
原理 当 CPU 要写数据时,只修改 Cache 里的内容 ,不 立刻去改主存。 只有当这块数据在 Cache 里待不住了,要被踢走(替换/Evict)的时候 ,才把它写回主存。
关键角色:脏位 (Dirty Bit) Cache 行中需要增加一个标记位,叫“脏位”。
Dirty = 1: 说明这行数据被修改过,和主存不一样(脏了)。
Dirty = 0: 说明这行数据没被改过,和主存一样(干净)。
生活比喻: 你花钱时,只在手里的草稿本上改。老板那边的正式账本先不管。 等到你的草稿本写满了,必须换新本子时(发生替换),你才看一眼旧本子:
如果本子上全是乱涂乱画(脏位=1),你就把最终结果一次性汇报给老板。
如果本子上很干净没动过(脏位=0),直接把本子扔了就行,不用找老板。
优缺点
优点(极快):
速度快: 写操作都在高速 Cache 里完成,CPU 爽飞了。
带宽省: 如果你对同一个变量 i 做了 100 次 i++,直写要访问主存 100 次;而回写只需要在最后踢出时访问 1 次主存。
缺点(复杂、有风险):
硬件复杂: 需要维护“脏位”。
短暂的不一致: 在被写回之前,主存里的数据是旧的。
I/O 风险: 如果此时磁盘(DMA)直接去读主存,可能会读到旧数据(需要额外的同步机制来解决)。
1. 直写 (Write-Through) 口诀: “老实人、立刻同步”
原理 每当 CPU 要往 Cache 里写数据时,它会同时把数据写回主存。 即:写 Cache + 写主存 同时进行。
生活比喻: 你每花一笔钱,不仅记在草稿本上,还立刻 跑去老板办公室,把正式账本也更新了。
优缺点
优点(简单可靠):
一致性极好: 主存里的数据永远是最新的。
实现简单: 既然主存总是新的,当 Cache 里的这块数据需要被踢走(替换)时,直接丢弃就行,不需要做任何操作。
缺点(慢):
速度受拖累: CPU 跑得快,但主存太慢了。每次写操作 CPU 都要等主存写完,大大降低了速度。
总线太忙: 哪怕只改了一个字节,都要往主存发一次信号,总线带宽被占满了。
补救措施:写缓冲 (Write Buffer) 为了不让 CPU 傻等主存,通常会在 CPU 和主存之间加一个“写缓冲队列” 。 CPU 把数据扔进缓冲队列就立刻溜走去干别的,由硬件慢慢把队列里的数据写进主存。
2. 回写 (Write-Back) 口诀: “懒人智慧、秋后算账”
原理 当 CPU 要写数据时,只修改 Cache 里的内容 ,不 立刻去改主存。 只有当这块数据在 Cache 里待不住了,要被踢走(替换/Evict)的时候 ,才把它写回主存。
关键角色:脏位 (Dirty Bit) Cache 行中需要增加一个标记位,叫“脏位”。
Dirty = 1: 说明这行数据被修改过,和主存不一样(脏了)。
Dirty = 0: 说明这行数据没被改过,和主存一样(干净)。
生活比喻: 你花钱时,只在手里的草稿本上改。老板那边的正式账本先不管。 等到你的草稿本写满了,必须换新本子时(发生替换),你才看一眼旧本子:
如果本子上全是乱涂乱画(脏位=1),你就把最终结果一次性汇报给老板。
如果本子上很干净没动过(脏位=0),直接把本子扔了就行,不用找老板。
优缺点
优点(极快):
速度快: 写操作都在高速 Cache 里完成,CPU 爽飞了。
带宽省: 如果你对同一个变量 i 做了 100 次 i++,直写要访问主存 100 次;而回写只需要在最后踢出时访问 1 次主存。
缺点(复杂、有风险):
硬件复杂: 需要维护“脏位”。
短暂的不一致: 在被写回之前,主存里的数据是旧的。
I/O 风险: 如果此时磁盘(DMA)直接去读主存,可能会读到旧数据(需要额外的同步机制来解决)。
7 种基本的寻址方式
算法: 操作数 = A
通俗解释: “现钞交易” 。数据不存放在内存或寄存器里,而是直接包含在指令代码中。
优点: 速度最快(因为取指令的时候顺便就把数据取到了,不用再去查内存)。
缺点: 数值的大小受限(因为指令字长是有限的,放不下太大的数)。
2. 直接寻址 (Direct Addressing)
算法: $EA = A$ (有效地址 = 指令中给出的地址 A)
通俗解释: “拿钥匙开柜子” 。指令直接告诉你数据在内存的哪个房间号(地址)。
优点: 计算简单,不用复杂的变换。
缺点: 地址范围有限(指令里的位数有限,可能访问不了太大的内存空间)。
3. 间接寻址 (Indirect Addressing)
算法: $EA = (A)$ (指令给出的地址 A 里存的不是数据,而是数据的真实地址 )
通俗解释: “寻宝图的寻宝图” 。你去地址 A,发现里面是一张纸条,纸条上写的才是数据真正的藏身之处。
优点: 有效地址范围大(哪怕指令短,也能访问大内存)。
缺点: 慢!因为要访问两次存储器(第一次取地址,第二次取数据)。
4. 寄存器寻址 (Register Addressing)
算法: 操作数 = (R) (数据在寄存器 R 中)
通俗解释: “翻口袋” 。数据就在 CPU 自己的口袋(寄存器)里,伸手就来。
优点: 执行极快(不需要访问慢速的内存),指令也很短(寄存器编号很短)。
缺点: 寄存器数量很少,也就是口袋太少,装不下多少东西。
5. 寄存器间接寻址 (Register Indirect Addressing)
算法: $EA = (R)$ (寄存器 R 里存的是数据的内存地址)
通俗解释: “口袋里的钥匙” 。CPU 口袋(寄存器)里没数据,但有一把通往内存的钥匙(地址)。
优点: 扩大了寻址范围(比直接寻址强)。
缺点: 比纯寄存器寻址慢,因为还是要访问一次存储器。
6. 偏移寻址 (Displacement Addressing) 这是现代计算机最常用的方式,包含相对寻址、基址寻址、变址寻址 三种。
算法: $EA = A + (R)$ (地址 = 形式地址 + 寄存器内容)
通俗解释: “基准点 + 偏移量” 。比如“从学校大门(基准)往东走 100 米(偏移)”。它结合了直接寻址和寄存器间接寻址的特点。
优点: 非常灵活(方便做数组访问、程序浮动等)。
缺点: 计算稍微复杂一点。
7. 堆栈寻址 (Stack Addressing)
算法: $EA = 栈顶$
通俗解释: “拿最上面的盘子” 。不需要给地址,默认就是操作堆栈最顶端的数据。
优点: 指令极短(甚至不需要地址码)。
缺点: 不灵活,只能按先进后出 (LIFO) 的顺序拿数据。
寻址方式
2. 寄存器寻址 (Register Addressing)
3. 直接寻址 (Direct Addressing)
概念: 指令中直接给出数据在内存中的物理地址(或逻辑地址)。
特点: 简单直观,但指令字较长(因为地址通常很长),地址固定,不灵活。
公式: 有效地址 EA = 指令中的形式地址 A
生活比喻: 纸条上写着:“去打开101号柜子 ,吃掉里面的东西”。
代码举例:
代码段
1 MOV AX, [2000H] ; 将内存地址 2000H 里的数据取出,放入 AX
(CPU 直接拿着 2000H 去内存找数据)
4. 寄存器间接寻址 (Register Indirect Addressing)
概念: 指令给出一个寄存器,这个寄存器里存的不是数据 ,而是数据的内存地址 。
特点: 比直接寻址灵活,只需要修改寄存器的值,就可以访问不同的内存单元(适合遍历数组)。
公式: 有效地址 EA = (寄存器内容)
生活比喻: 纸条上写着:“去查看左口袋 里的纸条,那上面写着柜子号”。(假设左口袋里纸条写着101,你就去101号柜子找苹果)。
代码举例:
代码段
1 2 MOV BX, 2000H ; 先把地址放入 BX MOV AX, [BX] ; CPU去读 BX,发现是 2000H,于是去内存 2000H 取数
5. 隐含寻址 (Implied Addressing)
概念: 指令中不明显给出操作数地址,而是由操作码隐含规定了操作数在哪里。
特点: 指令极短。
生活比喻: 纸条上只写了一个字:“吃! ”。(默认你自己知道要吃嘴里的东西,或者拿手里的东西吃,不需要指明)。
代码举例:
代码段
1 CLA ; Clear Accumulator (清空累加器 ACC)
(这里没有给出操作数,但CPU默认知道要操作的是 ACC 寄存器)
6. 偏移寻址类 (Displacement Addressing) 这类寻址方式非常重要,它们的共同点是:有效地址 = 寄存器内容 + 形式地址(偏移量) 。 根据使用的寄存器不同,分为以下三种:
A. 基址寻址 (Base Addressing)
侧重点: 搬家(程序重定位) 。
机制: 基址寄存器 (BR) 存放程序的起始地址 (由操作系统管理),指令中给出偏移量 。
公式: EA = (BR) + A
举例:
你的程序被加载到内存的 10000H 位置(BR=10000H)。
指令说“访问第 5 个字节”(A=5)。
CPU 实际访问:10005H。
B. 变址寻址 (Indexed Addressing)
侧重点: 数组遍历 。
机制: 变址寄存器 (IX) 存放数组下标 (由用户改变),指令中给出数组首地址 。
公式: EA = (IX) + A
举例:
指令指定数组首地址是 2000H(A=2000H)。
循环第一次,变址寄存器 IX = 1。CPU 访问 2001H。
循环第二次,变址寄存器 IX = 2。CPU 访问 2002H。
C. 相对寻址 (Relative Addressing)
侧重点: 跳转(分支指令) 。
机制: 以程序计数器 (PC) 当前的值为基准,向前或向后跳多少步。
公式: EA = (PC) + A
举例:
当前执行到第 100 行代码 (PC=100)。
指令是 JMP +10(向下跳10行)。
CPU 计算出目标地址:110 行。
中央处理器:数据通路和控制器 CPU 的工作流程 取指令 (Fetch) :去仓库(内存)里把订单(指令)拿出来。
PC+1 送 PC :订单拿到手了,把指针指向下一张 订单的位置。(注意:在 MIPS 真实硬件中通常是 PC+4,因为一条指令占4个字节,这里 PPT 用 “PC+1” 是为了简化逻辑,假设按“个”来数指令)。
指令译码 (Decode) :看懂订单。比如 “001010” 代表什么?哦,原来是“加法”。
取操作数 :加法需要两个数,去寄存器里把这两个数拿出来。
运算/访存 :ALU 开始干活(算加法),或者去读写内存。
存结果 :把算好的数写回寄存器。
异常/中断检查 :干完活了,检查一下有没有出乱子(异常),或者外面有没有人敲门(中断)。
组成指令功能的四种基本操作 Load :内存 $\rightarrow$ 寄存器 (去仓库拿材料)。
Store :寄存器 $\rightarrow$ 内存 (把成品放回仓库)。
Move :寄存器 $\rightarrow$ 寄存器 (左右手倒腾)。
ALU :运算 $\rightarrow$ 寄存器 (加工处理)。
读内存 :$R[r] \leftarrow M[addr]$ —— 控制器指挥数据从 Memory 流向 Register。
写内存 :$M[addr] \leftarrow R[r]$ —— 控制器指挥数据从 Register 流向 Memory。
内部搬运 :$R[a] \leftarrow R[b]$ —— 寄存器之间倒手。
运算 :$R[a] \leftarrow R[b] + R[c]$ —— 数据流经 ALU 进行加工。
CPU 的内部架构 CPU 拆成了两个独立但协作的部分:Control(控制器) 和 Datapath(数据通路) 。
Datapath(数据通路)—— 它是“肢体”
定义 :数据流经的路径和部件(如 ALU、寄存器、多路选择器)。
作用 :它负责干脏活累活 。
比喻 :它就像是工厂里的流水线设备和工人。你要它搬砖,它就搬砖;要它算加法,它就只能算加法。它自己没有思想,不知道什么时候该干什么。
Control(控制器)—— 它是“大脑”
定义 :根据指令(0和1的代码),生成控制信号。
作用 :它负责发号施令 。
比喻 :它就是拿着大喇叭的工头。
它看一眼指令(比如是 add),就会对 Datapath 喊:“ALU 准备做加法!寄存器准备存结果!”
它看一眼指令(比如是 load),就会喊:“内存接口打开!把数据读进来!”
总结 :我们后面要学的“设计 CPU”,其实就是先画出Datapath (把路铺好),然后设计Control (让信号指挥数据怎么走)。
数据通路——组合逻辑元件 1. 加法器 (Adder)
作用 :最简单的数学工。只干一件事:把输入的 A 和 B 相加。
用在哪 :专门用来算 PC + 4(计算下一条指令地址),或者算内存地址。
特点 :不需要复杂的控制,给电就由 A+B 出结果。
2. 多路选择器 (MUX) —— 非常关键!
通俗理解 :它是“铁路道岔” 或者“单刀双掷开关” 。
作用 :它有两个输入(A 和 B),但出口(Y)只有一个。谁能通过? 由红色的 Select(控制信号) 决定。
如果 Select 是 0,A 通过。
如果 Select 是 1,B 通过。
为什么重要 :CPU 内部经常面临选择。例如:写回寄存器的数据,是来自 ALU 的计算结果,还是来自内存的读取结果?这时候就需要用 MUX 来选。
3. 算术逻辑部件 (ALU)
作用 :全能数学工。能做加减乘除,也能做与或非。
控制信号 (OP) :这是关键!ALU 到底做“加法”还是“减法”,全靠红色的 OP 信号来指挥。这个 OP 信号就是“控制器”发给它的。
输出 Zero :这是一个反馈信号。如果计算结果是 0,Zero 线这就变 1。这通常用于判断 beq(相等跳转)指令。
4. 译码器 (Decoder)
作用 :翻译官。
原理 :输入 n 位二进制,输出 $2^n$ 条线。比如输入 001,它就让第 1 号线通电;输入 111,就让第 7 号线通电。
用在哪 :主要用于寄存器堆 (Register File)的选择。比如指令说“读 3 号寄存器”,译码器就把 3 号寄存器的门打开。
存储元件: 寄存器和寄存器组
1. 从“存 1 位”进阶到“存 32 位” 你刚才已经学会了 D 触发器 (只能存 1 个 bit,比如 0 或 1)。但现代 CPU(比如 32 位 MIPS)处理数据是一次性处理 32 位的。
寄存器 (Register) : 其实就是把 32 个 D 触发器 并排捆在一起。
Data In/Out :一次吞吐 32 位数据。
Write Enable (WE, 写使能) :这是一个“安全锁” 。
如果是 0:就算时钟边沿(Clock)来了,也不许修改里面的数据。
如果是 1:时钟边沿一来,允许写入新数据。
为什么要这个锁? 因为时钟一直在跳,但我们不是每个周期都要改写这个寄存器,只有需要存结果时才打开锁。
2. 寄存器堆:CPU 的“办公桌” CPU 只有 1 个寄存器是不够的,MIPS 架构里有 32 个通用寄存器。把这 32 个寄存器堆放在一起,就叫 寄存器堆 (Register File) 。
这一页重点讲了它的端口(Interface) ,也就是它的“进出口”:
两个“读”端口 (Read Ports) :
输入 :RA (Read Address A) 和 RB。比如你给 RA 输入 5,给 RB 输入 8。
输出 :busA 和 busB。它会立刻吐出 5 号和 8 号寄存器里存的数据。
性质 :组合逻辑 。意味着不需要时钟,不需要排队 ,只要地址给对了,数据立马出来(类似于你去查字典,翻到哪页字就在那)。
一个“写”端口 (Write Port) :
输入 :RW (Write Address,你要改几号箱子?)、busW (Write Data,你要存什么数据?)、Write Enable (允许修改吗?)。
性质 :时序逻辑 。意味着必须等时钟边沿 (Clock) 。就算你把数据准备好了,也没人理你,直到时钟“咔嚓”一声(上升沿),数据才会真正写进那个寄存器。
MIPS的三种指令类型 A. R-Type (Register Type) —— 纯寄存器操作
代表指令 :add, sub
格式特点 :
3 个寄存器 :它需要读两个源寄存器 (rs, rt),把结果写进目的寄存器 (rd)。
func 字段 :对于 R 型指令,op(操作码)通常都是 0,真正决定是加还是减的是最后的 func 字段。
硬件暗示 :我们需要寄存器堆提供两个读端口,一个写端口。
代表指令 :
运算类:ori (和常数做或运算)。
访存类:lw, sw (基于偏移量访问内存)。
分支类:beq (如果不相等,跳过多少条指令)。
格式特点 :
2 个寄存器 :rs 和 rt。
1 个立即数 :最后 16 位是一个常数 (immediate)。
硬件暗示 :
这里没有 rd 了!写回的目标变了(通常变成了 rt)。
这 16 位的数字太短了,而 CPU 是 32 位的。所以硬件里必须有一个 “扩展单元 (Sign/Zero Extender)” 把 16 位变成 32 位。
C. J-Type (Jump Type) —— 飞跃
代表指令 :j (Jump)
格式特点 :
硬件暗示 :这需要一个特殊的电路,直接把这 26 位塞进 PC (程序计数器) 里,让程序“瞬移”。
取指令部件(Instruction Fetch Unit)
路径一:去拿指令 (Fetch Instruction)
PC 寄存器 输出当前的地址(比如 1000)。
这个地址顺着线连到了 Instruction Memory (指令存储器) 的 Address 接口。
存储器立刻吐出地址 1000 里的那行代码 —— Instruction Word (32位指令字) 。
结果 :指令拿到手了,准备送去译码。这就是 RTL 里的 M[PC]。
路径二:准备下一条 (Update PC)
PC 寄存器 的输出(还是 1000)同时也连到了右边的 Next Address Logic (下地址逻辑) 。
对于大多数指令,这里的逻辑很简单,就是个加法器:1000 + 4 = 1004。
算出来的 1004 会绕回到 PC 的输入端,等待 下一个时钟信号到来。
结果 :为下一轮循环做好了准备。这就是 RTL 里的 PC <- PC + 4。
RR(R-type)型指令的数据通路
阶段一:读数据(原材料进场)
指令拆解 :
指令的 21-25位 (rs) 连接到寄存器堆的 Ra (Read Address 1) 接口。
指令的 16-20位 (rt) 连接到寄存器堆的 Rb (Read Address 2) 接口。
动作 :
寄存器堆收到地址后,立刻把这两个寄存器里的数据吐出来,分别送到 busA 和 busB 线路上。
阶段二:算数据(加工)
流入 ALU :
busA 和 busB 里的数据顺着线流进右边的 ALU 。
控制信号 ALUctr :
这时候,控制器 (图中虽未画出,但红字提到了)会给 ALU 发一个命令。
如果是 add 指令,ALUctr 信号就是“做加法”。
如果是 sub 指令,ALUctr 信号就是“做减法”。
结果产生 :ALU 算完后,结果出现在 Result 线路上。
阶段三:写回结果(成品入库) 这是最关键的一步,形成了一个闭环 。
数据回流 :
你看那条长长的线,从 ALU 的 Result 绕了一大圈,回到了寄存器堆左边的 busW (Write Data) 接口。
确定写给谁 :
指令的 11-15位 (rd) 连接到了 Rw (Write Address) 接口。这告诉寄存器堆:“把结果存进 rd 号格子里”。
关键钥匙 RegWr :
光把数据送回来还不行,必须有人开门。
RegWr (Register Write) 信号必须置为 1 。
时钟边沿 (Clk) :当时钟“咔嚓”一响(下降沿或上升沿),数据就被永久地锁进 rd 寄存器了。
带立即数的逻辑指令的数据通路
1. 右边的蓝色 MUX:解决“跟谁运算”的问题 请看 ALU 前面的那个蓝色梯形(多路选择器)。
冲突点 :
add 指令 想计算:寄存器 A + 寄存器 B。
ori 指令 想计算:寄存器 A or 立即数。
ALU 的第二个输入端(下端)很为难:我到底该连寄存器 busB,还是连立即数 imm16?
解决方案 (ALUSrc MUX) :
加一个 MUX!
0号通道 接 busB(给 R-Type 用)。
1号通道 接 ZeroExt 后的立即数(给 I-Type 用)。
控制信号 ALUSrc :如果是 ori 指令,控制器就把这个信号设为 1 ,强行把立即数送进 ALU。
2. 左边的蓝色 MUX:解决“结果存哪”的问题 请看寄存器堆 Rw(写地址)前面的那个蓝色梯形。这是初学者最容易晕的地方!
冲突点 :
R-Type (add rd, rs, rt) :结果要存进 rd (指令的第 11-15 位)。
I-Type (ori rt, rs, imm) :结果要存进 rt (指令的第 16-20 位)。因为 I 型指令里根本就没有 rd 这个字段!
寄存器堆的 Rw 接口很为难:我到底听谁的?
解决方案 (RegDst MUX) :
加一个 MUX!
0号通道 接 rd (11-15位)。
1号通道 接 rt (16-20位)。
控制信号 RegDst :如果是 ori 指令,我们必须把结果存回 rt,所以控制器把信号设为 1 。
3. 还有一个细节:ZeroExt (零扩展)
看 ALU 下方那个细长的蓝色方框。
为什么是 ZeroExt?
正如我们上一轮讨论的,ori 是逻辑运算,不关心正负号。它只需要把 16 位的立即数前面补 16 个 0,变成 32 位即可。
如果是 lw 指令,这里就会换成 SignExt(符号扩展)。
总结:一张控制信号表 根据这张图,如果要执行 ori rt, rs, imm16,我们的 “大脑”(控制器) 需要发出什么样的指令?(看 PPT 最下方蓝字)
RegDst = 1 :告诉寄存器堆,“把结果写进 rt (输入1)”。
RegWr = 1 :告诉寄存器堆,“请开门,我要写数据”。
ALUSrc = 1 :告诉 ALU,“你的第二个操作数是立即数 (输入1),别看寄存器 B”。
ALUctr = or :告诉 ALU,“做或运算”。
lw (Load Word) 指令的 RTL (寄存器传输级) 深度剖析1. RTL 流程再回顾 (Standard Steps) 这一页中间的蓝色字展示了 lw 指令执行的四个标准步骤:
取指 (Fetch) :M[PC]。老规矩,把指令搬进 CPU。
算地址 (Address Calc) :Addr <- R[rs] + SignExt(imm16)。
这是我们讨论过的重点。用基址寄存器 rs 加上扩展后的偏移量。
取数据 (Memory Access) :R[rt] <- M[Addr]。
拿着刚才算的地址,去 数据存储器 (Data Memory) 里把数据读出来,写进 rt 寄存器。
更新 PC :PC <- PC + 4。准备下一条。
2. 核心考点:为什么要“符号扩展”?(Sign Extension) 这是这张图最核心的技术细节。请看 PPT 下方的两个 32 位二进制条形图。
问题 :上一张 PPT 讲 ori 指令时用的是 ZeroExt (零扩展) ,为什么这里非要用 SignExt (符号扩展) ?
答案 :因为 地址偏移量可能是负数 !
场景 A (正向偏移) :如果你想访问 rs 后面 4 字节的地方,imm16 是 00...0100。符号位(第 15 位)是 0 。扩展成 32 位时,高位全补 0 。这和零扩展没区别。
场景 B (反向偏移) :如果你想访问 rs 前面 4 字节的地方,imm16 是负数(比如 -4,补码形式)。符号位(第 15 位)是 1 。
如果用零扩展:高位补 0,这个负数瞬间变成了一个巨大的正数,地址就指到九霄云外去了。
必须用符号扩展 :复制符号位 。如果第 15 位是 1,那么高 16 位全部填 1。这样 FFFF... 依然代表 -4,保证了数学上的正确性。
装入(lw)指令的数据通路
1. 为什么要加蓝色部分?(The “Why”) PPT 中间有个大大的问号:“加蓝色部分。为什么?”
新增部件:Data Memory (数据存储器)
原因 :lw 的核心任务是从内存取数。之前的电路只有 ALU 和寄存器,根本没有“仓库”可去。所以必须加上这个存储器单元。
动作 :ALU 算出的结果(比如 1004)不再被当作计算结果,而是变成了地址 (Address) 输入给存储器。
新增部件:MemtoReg MUX (右侧的多路选择器)
原因 :这是数据回流的关键路口。
以前做 add 时,写回寄存器的是 ALU 的结果 。
现在做 lw 时,写回寄存器的是 Memory 的结果 。
冲突 :一条写回线 (busW) 不能同时接两个输入。
解决 :加一个开关(MUX)。
通道 0:来自 ALU(给 R-Type 用)。
通道 1:来自 Memory(给 lw 用)。
控制信号 MemtoReg 决定走哪条路。
2. 数据的“长征”路线 (Data Flow) 跟着箭头走,看 lw rt, rs, imm16 是怎么执行的:
准备数据 :
从寄存器堆读出 rs (基地址) 。
把 imm16 (偏移量) 进行 符号扩展 (Sign Ext) (注意图下方的 ExtOp=1,代表 SignExt)。
计算地址 (ALU Stage) :
ALUSrc = 1 :ALU 的下端输入选择扩展后的立即数。
ALUctr = add :ALU 执行加法,计算出 基址 + 偏移。
访问内存 (Memory Stage) :
ALU 的计算结果连到了 Data Memory 的 Addr 端。
MemWr = 0 :我们是读内存,不是写内存,所以写使能关闭。
数据从 Data Out 吐出来。
写回寄存器 (Write Back Stage) :
MemtoReg = 1 :右边的蓝色 MUX 拨到 1 号位,允许内存的数据通过。
RegDst = 0 :左上角的 MUX 拨到 0 号位。为什么?因为 lw 的目标寄存器是 rt (16-20位),而不是 rd。
RegWr = 1 :最后,寄存器堆的大门打开,数据写入 rt。
3. 底部红字:控制信号大揭秘 PPT 最下方列出了执行 lw 所需的一整套“密码”(控制信号),我们来逐个破解:
RegDst = 0 :
写回目标是 rt (指令的 16-20 位),所以选 0 通道。
RegWr = 1 :
ALUctr = add :
ExtOp = 1 :
关键! lw 的偏移量是有符号的(可能往前偏移),所以必须用符号扩展 (Sign Extension),不能用零扩展。
ALUSrc = 1 :
ALU 的第二个操作数是立即数(偏移量),不是寄存器 B。
MemWr = 0 :
MemtoReg = 1 :
最关键的区别位 。这一位告诉 CPU:“这次写回的数据来自内存 ,不是 ALU”。
存数(sw)指令的数据通路 1. 核心动作:存数 (Store) 看看 PPT 顶部的公式:
这句话的意思是:
算地址 :用 rs 里的基地址 + 扩展后的偏移量,算出内存地址(和 lw 一模一样)。
存数据 :把 rt 寄存器里的值,写进刚才算出的那个内存地址里。
2. 蓝色部分:数据怎么流进内存? 图中间有一条醒目的蓝色折线 ,这是 sw 指令独有的特征:
起点 :busB 。
寄存器堆的 Rb 端口读取了 rt 寄存器的数据,送到了 busB 线路上。
注意 :在 R-Type 指令里,busB 是送去 ALU 做运算的;但在 sw 指令里,busB 里装的是要存进内存的货物 。
终点 :Data Memory 的 Data In 接口 。
这条线绕过了 ALU,直接插进了数据存储器的“写入口”。
为什么加这一条?
因为 ALU 忙着算地址去了(看 busA 和扩展后的立即数进了 ALU),它没空处理数据。
所以我们需要一条“旁路”,把寄存器里的数据直接送到内存门口等待写入。
3. 控制信号大反转 (The Control Signals) PPT 底部那一排蓝色的控制信号,有很多独特之处,特别是出现了 x (Don’t Care) 。
MemWr = 1 (最关键!)
这是 sw 的灵魂。必须把“写内存使能”打开,否则数据只会在门口蹭蹭,进不去。
RegWr = 0 (千万别写寄存器!)
sw 是往外存东西,不修改 CPU 内部的寄存器。
如果这里设为 1,那你就会意外地破坏寄存器里的数据。
RegDst = x (无关项)
既然 RegWr 已经是 0 了(寄存器大门紧闭),那么 RegDst 到底是选 rt 还是 rd 根本无所谓,反正也写不进去。这就是“Don’t Care”。
MemtoReg = x (无关项)
同理,既然不写回寄存器,那么右边那个 MUX 选内存的数据还是 ALU 的数据也无所谓了。
ALUctr = add & ALUSrc = 1 & ExtOp = 1
这三个信号和 lw 完全一样 。
因为不管是存还是取,“计算内存地址” 这个步骤是一模一样的(都是基址 + 符号扩展偏移量)。
条件转移指令的数据通路
第一部分:逻辑分析 (RTL) —— CPU 是怎么“思考”的? (对应图 image_2da7d8.png)
beq rs, rt, imm16 的核心逻辑分三步走:
比较 (Compare) :
RTL : Cond <- R[rs] - R[rt]
原理 :计算机比较两个数是否相等,最常用的办法就是做减法 。
如果 rs - rt 的结果是 0 ,说明它们相等;否则就不相等。
决策 (Decision) :
RTL : if (COND eq 0)
看刚才减法的结果。如果是 0,就跳转;如果不是 0,就忽略,继续执行下一条。
计算目标地址 (Target Address) —— 这是最难懂的地方!
公式 : PC <- PC + 4 + (SignExt(imm16) x 4)
为什么要 PC + 4? :MIPS 的分支是相对于“下一条指令”而言的。
为什么要 SignExt? :因为你可能往回跳(循环),也就是立即数可能是负数,所以要符号扩展。
为什么要 x 4?(红字思考题) :
指令里的 imm16 是以 “字 (Word)” 为单位的(比如“往下跳 2 条指令”)。
但内存地址是以 “字节 (Byte)” 为单位的(每条指令占 4 字节)。
所以硬件上必须把它 左移 2 位 (相当于 $\times 4$),才能变成真实的物理地址偏移量。
第二部分:硬件连线 (Data Path) —— 电路是怎么连的? (对应图 image_2da7de.png)
为了实现上面的逻辑,我们需要对电路做几个关键调整(看图中的蓝色连线):
1. ALU 的新用法:判零
输入 :busA (来自 rs) 和 busB (来自 rt)。
ALUSrc = 0 :注意! 这里必须选 0。因为我们是比较两个寄存器 ,不再是寄存器和立即数运算了(区别于 ori 和 lw)。
ALUctr = sub :让 ALU 做减法。
输出 Zero :看 ALU 右侧那根蓝色的线 Zero 。
这是 ALU 的一个特殊输出端口。
当运算结果为 0 时,这根线变成高电平(1)。这根线就是“相等”的信号灯 。
2. Next Addr Logic(下地址逻辑):CPU 的方向盘
图右边的那个 蓝色方框 是这一页的新增核心。
输入 :
PC (当前地址)
imm16 (偏移量)
Branch (控制信号:我是分支指令!)
Zero (状态信号:它们相等吗?)
逻辑 :
如果 Branch = 1 且 Zero = 1:说明是分支指令且条件满足 $\rightarrow$ 跳! (PC = Target Address)。
否则:不跳! (PC = PC + 4)。
3. 控制信号 (Control Signals) 看 PPT 最下方的蓝字设置,非常讲究:
RegWr = 0 :千万别忘了这个! beq 只是看一眼寄存器的值做比较,不修改 任何寄存器。如果设为 1,数据就乱了。
ALUSrc = 0 :我们要比较 busB (寄存器),不比较立即数。
Branch = 1 :告诉下地址逻辑,“准备好,可能要变道了”。
RegDst, MemtoReg = x :因为都不写寄存器,所以这些无关紧要 (Don’t Care)。
下址逻辑设计方案
1. 核心原理:为什么敢扔掉最后 2 位? (对应 image_2dc27e.png 的中间文字)
规律 :MIPS 指令长 32 位(4 字节)。因此,所有指令的内存地址一定是 4 的倍数(0, 4, 8, 12, …)。
二进制特征 :
0 -> 0000
4 -> 0100
8 -> 1000
12 -> 1100
发现 :不管怎么变,二进制的最后两位永远是 00 。
优化思路 :既然最后两位永远是 0,存在寄存器里也是浪费空间,算加法时还要多算两位。不如 PC 寄存器只存高 30 位 。
2. 新的数学逻辑:把“米”变成“步” (对应 image_2dc27e.png 的蓝色公式)
如果我们把地址看作“步数”(Words)而不是“字节数”(Bytes):
A. 顺序执行 (PC + 4 变成 PC + 1)
旧逻辑 (32位) :$PC \leftarrow PC + 4$。
新逻辑 (30位) :$PC \leftarrow PC + 1$。
(因为丢掉了两个 0,二进制的 100 00 变成了 100。加 1 就变成了 101,也就是原来的 101 00 )。
好处 :加法器只需要加 1,电路更简单。
B. 分支跳转 (x 4 消失了?)
旧逻辑 :$PC + 4 + (Imm16 \times 4)$。
新逻辑 :$PC + 1 + Imm16$。
还记得立即数 Imm16 本来存的就是“几条指令”吗?
以前我们需要把它 $\times 4$ 变成字节地址,再跟 32 位 PC 相加。
现在 PC 本身存的就是“几条指令”(30位),所以直接相加 就行了!不用移位了!
3. 硬件电路怎么连?(视觉解读) (对应 image_2dc282.png)
请跟着图里的线走,看看这 30 位 PC 是怎么还原成 32 位地址去取指的:
PC 寄存器 (左上角) :
你看那个 PC 盒子,旁边写着 30 。它只存 30 个 bit。
第一个加法器 (蓝色 “Adder”) :
输入是 “1” 。
它计算 $PC + 1$,也就是下一条指令的“行号”。
第二个加法器 (Branch Adder) :
把 $PC+1$ 的结果,加上 SignExt 出来的立即数。
注意 :这里没有 Shift Left 2 模块了!直接加。
最右边的拼接 (Concatenation) :
这是最骚的操作。
我们拿着算好的 30 位地址去访问 Instruction Memory。
Addr<31:2>31:2> :来自 PC 算出来的 30 位。
Addr<1:0>1:0> :硬连线接死,就是 “00” 。
结果 :30 位 + “00” = 完整的 32 位物理地址。
取指令部件 (Instruction Fetch Unit)
第一步:逻辑分析 (RTL) —— j 指令是怎么跳的? (对应图 image_2e277d.png)
指令格式 :j target
Target (26位) :指令中剩下的 26 位全部用来存目标地址。
核心公式:
这个公式在做什么?
这是一个 “拼凑法” (Pseudo-direct Addressing)。
高 4 位 (PC<31:28>) :保留当前 PC 的高 4 位。这意味着 j 指令不能跳得太远,只能在当前 256 MB ($2^{28}$ 字节) 的区域内跳转。
低 26 位 (target<25:0>) :直接用指令里带的 26 位地址替换掉原来的低位。
拼接 (concat) :把这 4 位和 26 位拼起来,刚好组成新的 30 位 PC 值。
这也是个优化点!
在标准 32 位设计中,target 通常需要左移 2 位 ($\times 4$) 才能用。但在这里,因为我们的 PC 本身存的就是“字地址”(第几行),所以不需要移位,直接拿过来拼上去就行!
第二步:硬件实现 —— 最终版取指部件 (对应图 image_2e27ba.png)
这张图展示了集大成者的电路。请注意图中新增的蓝色部分 :
1. 拼接逻辑 (Top Blue Oval)
输入 1 :从 PC 寄存器引出的线,取最高 4 位 (30位里的高4位)。
输入 2 :从指令 (Instruction Word) 里直接截取的低 26 位。
动作 :这两组线在电路板上物理合并,变成一束 30 位的线。
2. Jump MUX (右边的那个多路选择器) 这是 “最高优先级” 的开关。
位置 :它放在了 Branch MUX 的后面。
逻辑 :
0号通道 :来自前面的 Branch 逻辑(可能是 PC+1,也可能是 beq 跳转)。
1号通道 :来自刚才拼出来的 Jump 目标地址。
控制信号 Jump :
一旦控制器发现是 j 指令,把 Jump 设为 1 。
后果 :不管前面算出了什么(比如 beq 想不想跳),Jump 说了算 ,强制把 PC 修改为跳转目标。
指令与控制信号的关系表 1. 主控制信号真值表 (Main Control Table) 这是根据 op (操作码) 产生的控制信号:
信号名 (Signal)
R-type(add, sub)
ori
lw
sw
beq
jump
Opcode
000000
001101
100011
101011
000100
000010
RegDst
1 (rd)
0 (rt)
0 (rt)
x
x
x
ALUSrc
0 (RegB)
1 (Imm)
1 (Imm)
1 (Imm)
0 (RegB)
x
MemtoReg
0 (ALU)
0 (ALU)
1 (Mem)
x
x
x
RegWrite
1 (写)
1 (写)
1 (写)
0 (不写)
0 (不写)
0 (不写)
MemWrite
0 (不写)
0 (不写)
0 (不写)
1 (写)
0 (不写)
0 (不写)
Branch
0
0
0
0
1 (分支)
0
Jump
0
0
0
0
0
1 (跳转)
ExtOp
x
0 (零扩)
1 (符扩)
1 (符扩)
x
x
ALUOp (注1)
R-Type
Or
Add
Add
Sub
xxx
图例说明:
1 : 信号有效(如选中 MUX 的 1 号通道,或使能写入)。
0 : 信号无效(如选中 MUX 的 0 号通道,或禁止写入)。
x (Don’t Care) : 任意值。因为在该指令下,这个信号控制的部件结果不会被使用(例如 sw 指令不写寄存器,所以 RegDst 选谁都无所谓)。
2. ALU 控制逻辑 (ALU Control Logic) 对于 R-Type 指令,ALU 具体做什么操作(加还是减),不仅取决于主控制信号,还取决于指令末尾的 func (功能码) 。
这是 PPT image_2f0f80.png 底部展示的二级解码逻辑:
指令类型
ALUOp (来自主控)
func (来自指令)
ALUctr (输出给ALU)
操作含义
lw / sw
Add (000)
xxxxxx
Add
计算地址
beq
Sub (100)
xxxxxx
Sub
比较相等
ori
Or (010)
xxxxxx
Or
逻辑或
R-type (add)
R-Type (XXX)
100000
Add (001)
加法运算
R-type (sub)
R-Type (XXX)
100010
Sub (101)
减法运算
3. 关键信号复习 (根据表格总结)
RegDst (寄存器目标) :
1 (R-Type):结果存入 rd (11-15位)。
0 (lw/ori):结果存入 rt (16-20位)。
ALUSrc (ALU源) :
0 (R-Type/beq):ALU 第二个操作数来自 寄存器 。
1 (lw/sw/ori):ALU 第二个操作数来自 立即数 。
ExtOp (扩展操作) :
1 (lw/sw):符号扩展 (SignExt),用于计算地址偏移。
0 (ori):零扩展 (ZeroExt),用于逻辑运算。
MemtoReg (内存到寄存器) :
1 (lw):写入寄存器的数据来自 内存 。
0 (R-Type/ori):写入寄存器的数据来自 ALU 。
什么是流水线处理器 . 核心原理:从“串行”到“并行” 在传统的单周期处理器 中,一条指令必须彻底跑完“取指、译码、执行、访存、写回”所有环节,下一条指令才能开始。而流水线处理器 将这些环节切分开:
并行化 :当第一条指令进入“执行”阶段时,第二条指令可以同时进入“译码”阶段,第三条指令进入“取指”阶段。
理想状态 :在每个时钟周期的末尾,流水线的终点都会“吐出”一条执行完毕的指令。
2. MIPS 的标准五级流水线 典型的 MIPS 处理器将指令处理分为 5 个标准步骤(Stage):
IF (Instruction Fetch) :从指令存储器中读取指令。
ID (Instruction Decode) :翻译指令含义,并从寄存器堆读取操作数。
EX (Execute) :使用 ALU 进行算术/逻辑运算,或计算内存地址。
MEM (Memory Access) :如果是 lw 或 sw 指令,则访问数据存储器。
WB (Write Back) :将运算结果或读出的数据写回寄存器堆。
3. 为什么流水线更高效?
缩短时钟周期 :单周期的时钟频率受限于最慢的指令(如 lw)。流水线将长路径切成短路径,使得 CPU 可以运行在更高的频率下。
提高吞吐率 :虽然单条指令从开始到结束的时间(潜伏期)没有减少,但单位时间内完成的指令总数大大增加了。
4. 流水线面临的挑战:冒险(Hazards) 流水线虽然快,但也会带来副作用,即不同指令在流水线中“追尾”或争抢资源的情况:
结构冒险 :多个指令同时需要同一个硬件资源(例如同时要读指令和写数据)。
数据冒险 :后面的指令需要前面指令还没算出来的结果。
控制冒险 :遇到 beq 或 j 指令时,CPU 还没确定跳不跳,后面的指令已经提前进场了。
指令流水线阶段汇总表
指令类型
IF (取指)
ID (译码/读寄存器)
EX (执行)
MEM (访存)
WB (写回)
Load (lw)
取指令
读基址寄存器
计算内存地址
读取内存数据
写回目标寄存器
Store (sw)
取指令
读基址及源数据
计算内存地址
写入数据到内存
NOOP (空操作)
R-type (add/sub)
取指令
读两个源寄存器
算术/逻辑运算
NOOP (空操作)
写回运算结果
Beq (分支)
取指令
读两个源寄存器
比较并算目标地址
条件成立则写PC
NOOP (空操作)
五阶段流水线数据通路
1. 核心物理组件:流水线寄存器 在电路图中,你可以看到四个明显的垂直红色矩形,它们是流水线的“灵魂”:
IF/ID Register :位于取指与译码之间。
ID/Ex Register :位于译码与执行之间。
Ex/Mem Register :位于执行与访存之间。
Mem/Wr Register :位于访存与写回之间。
为什么要增加这些寄存器?
保存执行结果 :用于保存每个阶段产生的中间数据(如运算结果、读取的数据、目标寄存器编号等),防止新指令进入流水线时覆盖旧指令的数据。
透明性 :它们属于内部寄存器,程序员不可见,也不需要作为程序现场保存。
2. 五阶段数据流向详解 在每个时钟周期的上升沿,数据通过流水线寄存器同步向右“迈进”一步:
IF (Ifetch) :通过 IUnit 取出指令并存入 IF/ID Register ,同时传递 PC + 4 地址。
ID (Reg/Dec) :从 RFile 读取 rs、rt 寄存器的值(busA/busB),进行立即数扩展,并将这些值连同目标寄存器编号存入 ID/Ex Register 。
Ex (Exec) :ALU 使用来自 ID/Ex 的数据进行计算。结果连同需要存入内存的数据(busB)存入 Ex/Mem Register 。
Mem :根据 Ex/Mem 传递的地址访问 Data Mem 。读取出的数据或 ALU 结果存入 Mem/Wr Register 。
Wr (WB) :数据流到最右侧,通过 Mux 选择最终要写回的数据,通过长长的绿线导回到左侧 RFile 的 RwDi 端 完成写回。
3. 控制信号的“接力”传输 在流水线中,控制信号(如 RegWr、ALUSrc、MemWr)不再是全局共享的,而是随指令一同移动 :
所有控制信号在 ID 阶段 被一次性生成。
信号被存入流水线寄存器中,像接力棒一样随着该指令向右传递。
例如,MemWr 信号必须等到该指令流动到 Mem 阶段 才会真正去控制数据存储器的写入。
最特殊的 RegWr(寄存器写使能)必须一路传到最后的 Mem/Wr 寄存器 ,才能确保在第五个周期准确控制写回操作。
4. 关键点:目标寄存器编号的同步 请特别注意 image_d2b224.png 下方的绿线。指令在 ID 阶段就知道要写回哪个寄存器(rd 或 rt),但这个编号必须一路通过 ID/Ex -> Ex/Mem -> Mem/Wr 传递,最后在 WB 阶段才作为 Rw(写地址)提供给寄存器堆。如果直接在 ID 阶段连线到 Rw,会导致当前在 WB 阶段的指令写错地方(写到了新进场指令的目标地址里)。
指令部件 IUnit的设计
1. IUnit 的两大核心功能 在每个时钟周期内,IUnit 必须自动且确定地完成以下操作:
读取指令 (Instr <- Mem[PC]) :根据程序计数器(PC)提供的地址,从指令存储器(Instruction Memory)中取出当前要执行的指令字。
更新地址 (PC <- PC + 4) :使用专门的加法器(Adder)计算当前地址加 4 的结果,为读取下一条顺序执行的指令做准备。
2. IUnit 内部的关键硬件组件 结合 image_d3133f.png 的电路连线,IUnit 主要由以下部件构成:
PC (Program Counter) :保存当前正在执行指令的地址(例如图中显示的 PC = 16)。
Instruction Memory (指令存储器) :以 PC 的值为地址输入,输出对应的指令字(例如 lw $1, 100($2))。
Adder (加法器) :专门用于执行自增 4 操作。请注意,PC + 4 的值除了用于更新下一次取指地址,还会被存入流水线寄存器中,用于后续阶段计算转移目标地址。
MUX (多路选择器) :位于 PC 输入端。其控制信号通常由其他阶段产生,用于在“顺序执行 (PC+4)”和“发生跳转 (Target Address)”之间做出选择。
3. 取指阶段的特殊逻辑:无需控制信号 这是一个非常关键的设计细节:在取指阶段(Ifetch),不需要根据指令的不同来控制执行不同的操作 。
确定性 :因为无论是什么指令(add、lw 还是 beq),第一步动作都是一样的——取指并加 4。
自动运行 :该阶段的功能是预先确定的,无需控制部件介入。
4. 数据的中转:IF/ID 流水线寄存器 取指完成后,IUnit 将其成果存入 IF/ID 寄存器 中:
存储内容 :必须保存指令字 (用于后续译码)和 PC + 4 的值 (用于计算分支跳转地址)。
输出时机 :这些信息总是存放在流水线段寄存器中,并在下一个时钟周期到来后的 Clock-to-Q 时刻输出给译码阶段。
流水线中的Control Signals如何获得?
1. 为什么只有后三个阶段有控制信号? 观察 image_d39301.png 可以发现,控制信号仅存在于 Exec (执行) 、Mem (访存) 和 Wr (写回) 这三个阶段。
Ifetch (取指) 和 Reg/Dec (译码) 阶段没有控制信号。
原因 :在这两个阶段,所有指令执行的功能完全一样(都是取指、加 4、译码、读寄存器),是确定性的操作,无需根据指令不同来控制硬件。
2. 控制信号的“生命周期” 控制信号的产生与流动像流水线上的货物一样有序:
产生 (ID 阶段) :控制器根据从 IF/ID 寄存器传来的指令操作码,一次性生成该指令在后续所有阶段需要的全部信号。
传递 (Pipeline Registers) :这些信号被锁存在流水线寄存器中,随着指令一同向右泵送。
Exec 阶段使用 :ALUSrc、ALUOp、RegDst 等。
Mem 阶段使用 :MemWr、Branch。
Wr 阶段使用 :RegWr、MemtoReg。
3. 以 Load 指令为例的信号路径 参考 image_d39301.png,看 lw 指令如何携带信号:
在 Exec 段 :从 ID/Ex 寄存器取出 ALUSrc=1 控制 Mux 选择立即数,取出 ALUOp=Add 让 ALU 算地址。
在 Mem 段 :信号跨过 Ex/Mem 寄存器。由于 lw 是读内存,MemWr 信号在此时应为 0。
在 Wr 段 :信号跨过 Mem/Wr 寄存器。此时 RegWr=1 有效,控制数据最终写回寄存器堆。
4. 关键点:反向数据流与冒险 PPT 提醒我们,流水线中存在反向数据流 (如 Wr 阶段写回寄存器堆,或 Mem 阶段将目标地址写回 PC)。
物理挑战 :这种反向流动不同于洗衣流水线,它可能引起数据冒险 (后面的指令需要前面还没写回的数据)或控制冒险 (跳转指令改变了取指顺序)。
解决方案 :这将引出后续关于旁路前递(Forwarding)和流水线停顿(Stalling)的讨论。
Load指令:流水线中的控制信号
1. 信号的产生:集中于 ID 阶段 所有的控制信号都是在 取数/译码(Reg/Dec)阶段 由主控制器(Main Control)根据指令的操作码一次性产生的。
延迟使用 :虽然信号在第 2 阶段就产生了,但 Load 指令在后续各个阶段(Exec, Mem, Wr)需要的操作各不相同,因此信号必须被“打包”送往后续阶段。
2. 信号的接力传递与使用 由于各阶段的操作是在不同的时钟周期完成的,控制信号必须保存在 流水线段寄存器 中,并随着指令一同向右泵送。
所属阶段
关键控制信号
使用时机
功能说明
Exec
ExtOp, ALUSrc, ALUOp, RegDst
1 个周期后 使用
控制 ALU 计算内存地址。例如 ALUSrc=1 选择立即数作为操作数。
Mem
MemWr, Branch
2 个周期后 使用
控制数据存储器的读写。对于 Load 指令,MemWr=0(只读)。
Wr
MemtoReg, RegWr
3 个周期后 使用
控制最后的数据回写。RegWr=1 开启寄存器堆写入开关。
3. 为什么信号也要保存在寄存器中? PPT 明确指出:“控制信号也要保存在流水段寄存器中! ”
同步性 :流水线中同时运行着多条指令。如果 RegWr 信号直接从 ID 阶段连到最后的写回端,那么当前正在 ID 阶段的指令就会错误地控制正在 WB 阶段的指令是否写回寄存器。
逻辑一致性 :通过流水线寄存器(如 ID/Ex, Ex/Mem, Mem/Wr),控制信号与它所控制的数据始终保持“同步前进”,确保每个功能部件在处理某条指令时,拿到的是属于该指令的控制指令。
4. 关键点:1st 和 2nd 阶段为何没有信号? 在 image_d39301.png 中,我们可以看到取指(Ifetch)和译码(Reg/Dec)阶段上方没有控制信号线条。
原因 :这两个阶段的功能对每一条指令来说都是一样的 (都是取指、加 4、译码、读寄存器)。
特征 :这些操作是确定性的,不需要根据指令的不同来控制硬件执行不同的操作。
流水线的三种冲突/冒险(Hazard)情况 1. 结构冒险 (Structural Hazards) 结构冒险 也称为资源冲突 ,是指硬件资源不足,导致多条指令在同一周期内试图使用同一个物理部件。
典型现象 :
两条指令试图同时写入寄存器堆。例如,Load指令在第5阶段写回,而后续的R-type指令若在第4阶段就写回,会发生“写口”竞争。
解决方法 :
规整化设计 :规定每个功能部件每条指令只能使用1次,且必须在特定周期使用。例如,强制所有指令都在第5阶段(Wr)使用寄存器写口,即使是原本只需4阶段的指令也需加入“空NOP”阶段来对齐。
增加硬件资源 :设置多个独立的部件以避免冲突。如将指令存储器(IM)和数据存储器(DM)分开设计。
2. 数据冒险 (Data Hazards) 数据冒险 也称为数据相关 ,是指后面的指令需要用到前面指令尚未产生或尚未写回的结果数据。
典型现象 :
前面的指令计算结果还没写回寄存器,后面指令就已经进入译码阶段准备读取该寄存器了。
解决方法 :
转发/旁路技术 (Forwarding/Bypassing) :在结果产生后直接通过硬件连线传给需要的部件,而不必等待写回寄存器。
流水线阻塞 (Stall) :对于某些情况(如Load-use冒险),必须停顿一个周期。
编译器优化 :通过重新排列指令顺序来减少冲突。
3. 控制冒险 (Control Hazards / Branch Hazards) 控制冒险 是指由于程序流方向发生改变(如执行分支、跳转或异常),导致已经在取指阶段取出的指令变得无效。
典型现象 :
在条件分支指令(如beq)的目标地址确定之前,后续指令已经被取进流水线了。
解决方法 :
分支预测 :采用静态或动态预测技术预估分支是否跳转。
编译器优化 :利用“分支延迟槽(Branch Delay Slot)”技术,在分支指令后放置一条无论跳转与否都要执行的有用指令。
数据冒险
1. 什么是数据冒险? 数据冒险是指在流水线执行过程中,当后面的指令需要用到前面指令尚未产生或尚未写回的结果数据时,所引发的冲突。这种现象会导致流水线无法正常执行后续指令,进而引起阻塞或停顿。
2. 核心实例分析:寄存器 r1 的冲突 参考 image_d4efbc.png 中的指令序列,我们可以清楚地看到 RAW(写后读) 冒险是如何发生的:
add r1, r2, r3 :这条指令的目标是将计算结果写入寄存器 r1 。
sub r4, r1, r3 :紧随其后的指令需要读取 r1 的值。
冲突点 :当 sub 指令在 Reg/Dec 阶段读取 r1 时,add 指令正处于 Exec 阶段执行加法。此时,r1 中的值还是旧的,新值尚未写回。
and r6, r1, r7 :
冲突点 :当 and 读取 r1 时,add 处于 Mem 阶段传递结果。此时读取的依然是旧值。
or r8, r1, r9 :
冲突点 :当 or 读取 r1 时,add 正处于 Wr (WB) 阶段。虽然正在写回,但在时钟前半周期读取时,拿到的往往还是旧值。
xor r10, r1, r11 :
正常点 :直到这条指令,add 已经彻底完成了写回操作,xor 才能读到 r1 的新值 。
方案1: 在硬件上采取措施,使相关指令延迟执行
1. 方案核心原理:插入“气泡” 当硬件检测到指令间存在数据依赖(例如后续指令需要使用尚未写回的数据)时,会强制阻止后续指令继续执行,直到数据产生并写回为止。
流水线阻塞 (Stall) :这种做法形象地被称为插入 “气泡 (Bubble)” 。
执行逻辑 :相关指令会被延迟,直到前序指令完成写回(WB)操作,新值已存入寄存器堆后,后续指令才开始取数(Reg)阶段。
2. 实例分析:延迟三个时钟周期 参考 image_d4f7a2.png 中的时序图,我们可以看到 add 指令与后续 sub 指令的冲突处理:
冲突源 :add r1, r2, r3 要在第 5 个周期(WB)才能将新值写回寄存器 r1 。
阻塞过程 :
为了确保 sub r4, r1, r3 能读到 r1 的新值,硬件在 add 执行期间强制插入了 3 个周期 的阻塞(stall)。
在图中,这表现为 sub 指令在 ID/RF(译码/读寄存器)阶段原地踏步,直到 add 指令完成红色的 Reg 写回操作。
结果 :sub 指令最终被延迟了三个时钟周期才开始正式执行。
3. 该方案的优缺点评价 根据 PPT 的总结,虽然该方案解决了数据正确性问题,但也带来了明显的代价:
优点 :保证了数据的绝对正确,能够处理所有类型的数据冒险。
缺点 :
性能下降 :指令被大幅延迟,导致流水线的整体效率(吞吐率)显著降低。
设计复杂 :控制逻辑变得非常复杂,因为需要对数据通路进行大量修改,以实现“暂停”和“等待”的功能。
方案 2: 软件上插入无关指令
1. 方案核心原理:显式插入 NOP 当编译器检测到指令序列中存在数据依赖(例如 sub 指令必须等待 add 指令写回结果才能开始读取)时,它会在两条有冲突的指令之间强行插入若干条 NOP(空操作)指令 。
执行逻辑 :这些 NOP 指令在流水线中正常“空转”,唯一的作用就是拉开冲突指令之间的时间距离。
目的 :确保后续指令在进入译码阶段(ID/RF)时,前序指令已经完成了写回(WB)操作,从而能读到寄存器中的最新值。
2. 实例分析:插入三条 NOP 指令 参考 image_d54dd5.png 中的时序图,我们可以看到对 r1 寄存器冲突的处理:
冲突源 :add r1, r2, r3 的结果直到第 5 个周期(WB)才会正式存入寄存器。
软件干预 :编译器在 add 指令之后连续插入了 三条 nop 指令 。
结果 :
sub r4, r1, r3 被推后到了第 5 个周期才进入取数阶段。
此时,add 指令正好在写回(图中红色的 Reg 块),sub 指令能够顺利拿到 r1 的新值。
3. 方案评价:优缺点对比 根据 PPT 的总结,这是一种牺牲效率换取简单的做法:
优点 :
数据通路简单 :硬件不需要复杂的冲突检测逻辑或暂停机制。
零硬件改造成本 :直接在原有数据通路上运行即可。
缺点 :
浪费严重 :正如 PPT 所说,这“浪费三条指令的空间和时间,是最差的做法 ”。
代码膨胀 :生成的二进制文件会因为包含大量没意义的 NOP 而变得臃肿。
方案3: 同一周期内寄存器堆先写后读
1. 方案核心原理:利用时钟脉冲的边缘 该方案的核心在于对寄存器堆(Register File) 读写时序的精细控制。
硬件基础 :寄存器堆的读口和写口是相互独立的物理部件。
操作逻辑 :在一个时钟周期内,我们将操作分为两个阶段:
前半周期 :进行写操作 。让处于写回(WB)阶段的指令将数据存入寄存器。
后半周期 :进行读操作 。让处于译码(ID/RF)阶段的指令读取寄存器中的数据。
结果 :刚写入的数据可以被立即读出,从而在同一个时钟周期内完成数据的“接力”。
2. 实例分析:r1 寄存器的即时传递 参考 image_d5515c.png 中的时序图,观察 add 指令与后续指令的交互:
冲突点缓解 :在 Cycle 5 时,add r1, r2, r3 正在进行 WB (图中深红色的写块)。
即时读取 :与此同时,or r8, r1, r9 正在进行 ID/RF (图中淡红色的读块)。
成功匹配 :由于采用了“先写后读”策略,or 指令在 Cycle 5 的后半部分读取时,拿到的是 add 在前半部分刚刚存入 r1 的新值。
3. 方案评价:局限性与价值
优点 :不需要增加额外的复杂连线(如转发电路),也不需要插入 NOP 或阻塞,利用现有的寄存器堆结构就能解决一部分冒险。
局限性 :
只能解决部分冒险 :正如 PPT 所言,它“只能解决部分数据冒险 ”。
时间距离限制 :它只能解决那些“读”和“写”刚好发生在同一周期的冲突(即指令间距为 2 的情况,如 add 和 or)。
无法处理相邻冲突 :如果 sub 指令紧跟在 add 后面(间距为 1),在 sub 需要读取数据时,add 还处于执行阶段,数据根本还没算出来,这种方案就无能为力了。
方案4: 利用DataPath中的中间数据:转发+阻塞
1. 方案核心原理:数据“抄近路” 转发技术,也称为旁路(Bypassing) ,其核心思想是:不等结果写回寄存器堆,直接从流水段寄存器中把数据“截胡”送给需要的部件 。
观察点 :虽然 add r1, r2, r3 还没把结果存入 r1 寄存器,但实际上计算好的值已经存在于 Ex/Mem 或 Mem/Wr 级流水段寄存器中了。
动作 :硬件通过增加专门的连线(旁路),直接把这些中间值引回到 ALU 的输入端。
2. 实例分析:r1 的实时传递 参考 image_d5609b.png 中的时序图,我们可以看到红色箭头代表的转发路径:
与 sub 的冲突 :当 sub 处于 EX 阶段需要 r1 时,add 刚算完结果并存放在 Ex/Mem 寄存器中。硬件直接将该值通过红色斜线传给 ALU 的输入。
与 and 的冲突 :当 and 处于 EX 阶段时,add 的结果已经流动到了 Mem/Wr 寄存器。硬件同样通过旁路连线将其直接喂给 and 指令的 ALU。
结果 :这两条原本需要停顿 3 个周期的指令,现在可以紧跟在 add 后面执行,无需任何停顿 。
3. 为什么还需要“阻塞(Stall)”? 虽然转发能解决大部分问题,但有一种特殊情况无法单纯靠转发解决,即 Load-use 冒险 :
冲突点 :如果前一条指令是 lw r1, 0(r2),数据直到 Mem 阶段结束(即 Cycle 4 结束)才从存储器读出。
物理限制 :如果下一条指令在 Cycle 4 的 Exec 阶段就要用这个值,由于时间轴上“读出”晚于“计算”,数据无法向前穿越时间。
对策 :此时硬件必须强制插入一个 Stall(气泡) ,将后续指令推后一个周期,然后再配合转发技术获取数据。
4. 方案评价
优点 :极大地提高了流水线的吞吐率。在大多数 R 型指令相关的冲突中,它消除了所有停顿,让 CPU 保持满速运行。
代价 :增加了硬件设计的复杂性。需要增加大量的转发多路选择器(Mux)和复杂的转发控制逻辑 (用于判断当前 ALU 需要的数据是否正在流水线的后面几级中流动)。
硬件上的改动以支持“转发”技术
1. 核心硬件改动:引入多级反馈路径 在 image_d567fe.png 中,你可以看到 ALU 的输入端增加了显著的硬件变动:
增加多路选择器 (MUX) :在 ALU 的两个操作数输入端,原本直接连接 ID/Ex 寄存器的地方,现在各插入了一个 3 选 1 的 MUX 。
物理连线(旁路路径) :
EX 阶段转发(绿线) :从 Ex/Mem 寄存器 的输出(即刚算好的 ALUout)引回一根线,连接到 ALU 输入端的 MUX。
MEM 阶段转发(红线) :从 Mem/Wr 寄存器 的输出(即上一条指令读出的内存数据或算好的值)引回一根线,同样连接到该 MUX。
2. 不同冒险场景下的数据流向 根据图中提供的指令序列示例,硬件通过切换 MUX 的选择信号来实时捕获数据:
场景 1:R-R 型相邻指令冲突
指令:add r3, r2, r1 紧接 sub r5, r3, r4。
转发逻辑 :当 sub 在 ALU 计算时,add 的结果刚存入 Ex/Mem 寄存器。此时硬件切换 MUX,通过绿线 将 r3 的新值直接喂给 ALU,解决冒险。
场景 2:间距为 2 的 R 型指令冲突
指令:add r3, r2, r1 … sub r5, r3, r4。
转发逻辑 :此时 add 的结果已经流动到 Mem/Wr 寄存器。硬件切换 MUX,通过红线 将数据传回,确保 sub 拿到最新值。
3. 特殊限制:Load-use 冒险 PPT 在右侧提出了一个关键问题:lw r3, 100(r1) 后跟 or r6, r3, r1 能单纯靠转发解决吗?
物理瓶颈 :lw 指令的数据直到 Mem 阶段结束 才从数据存储器(DM)中流出。
时间轴冲突 :如果下一条指令 or 在同一个周期内就需要这个值进行 Exec 运算,数据在物理上还没读出来,无法“逆转时间”传回。
结论 :这种情况下,硬件必须先执行一次阻塞(Stall)\ 插入气泡,让 or 指令延后一个周期,再通过*红线路径* 完成转发。
Load指令引起的延迟现象
1. 现象分析:为什么会有延迟? Load 指令(如 lw)与普通的 R 型指令不同,它的数据产出时间点非常靠后:
数据产出时刻 :Load 指令在 Mem(访存) 阶段结束时,才能从数据存储器中读出数据。
物理限制 :实际上,在第四周期结束时,流水段寄存器(Mem/Wr)中才真正拥有后续指令需要的值。
冲突点 :如果紧随其后的第一条指令(Plus 1)在自己的 Exec 阶段(即第四周期)就需要用到这个值进行计算,此时数据尚未从存储器读出,转发技术也无法“逆转时间”将还没产生的数据传回去 。
2. 转发技术能解决什么? 通过 image_d5cd56.png 的时序图可以看到转发技术的局限与作用:
能解决的 :转发技术可以使 Load 指令后面 第二条指令 (Plus 2)得到所需的值。
图中蓝色的实线箭头显示:Load 产生的数据在第四周期结束时就位,正好可以转发给处于第五周期 Exec 阶段的 Plus 2 指令。
不能解决的 :它不能解决 Load 指令与随后的第一条指令 (Plus 1)之间的数据冒险。
图中红色的虚线箭头表示一个物理上不可能实现的请求:数据在第四周期末才出来,不可能传给在第四周期初就开始工作的 Plus 1。
3. 核心定义:装入-使用数据冒险 这种 Load 指令与其紧跟的指令之间因数据生产滞后而产生的冲突,被称为 “装入-使用数据冒险 (Load-use Data Hazard)” 。
4. 最终对策:延迟执行 为了保证程序的正确性,硬件或软件必须采取补救措施:
硬件阻塞 (Stall) :硬件会自动插入一个周期的“气泡”,将 Plus 1 指令及其后续指令全部推迟一个周期执行。
编译器优化 :编译器可以通过调整指令顺序,在 Load 指令后插入一条无关指令,从而利用这个间隙消除延迟。
流水线处理器的五个阶段 1. 五阶段流水线详解
阶段缩写
全称
核心任务
IF
Ifetch (取指)
根据 PC 地址从指令存储器中读取指令,并计算 PC+4。
ID
Reg/Dec (译码)
翻译指令含义,同时从寄存器堆(RFile)中读取操作数 rs 和 rt 的值。
EX
Exec (执行)
使用 ALU 进行运算。对于 Load/Store 计算内存地址;对于 R 型指令执行算术逻辑运算。
MEM
Mem (访存)
如果是 Load 则从内存读数据;如果是 Store 则往内存写数据。非访存指令此阶段仅传递结果。
WB
Wr (写回)
将最终结果(来自 ALU 或内存)写回到目标寄存器中,正式更新 CPU 状态。
2. 深度解析:EX/MEM 这种斜杠名称是什么意思? 你提到的 EX/MEM (以及 IF/ID, ID/EX, MEM/WB)并不是一个阶段,而是流水线寄存器(Pipeline Register) 。
命名逻辑 :它的名称代表它位于哪两个阶段之间 。例如,EX/MEM 就位于 执行(EX) 和 访存(MEM) 阶段的交界处。
物理本质 :它是一组触发器,用于锁存(保存)前一个阶段产生的成果。
具体到 EX/MEM 的作用 :
保存结果 :它保存了 EX 阶段 ALU 刚刚算出来的地址或运算结果。
传递控制信号 :它还保存了这条指令在后续 MEM 级(如 MemWr)和 WB 级(如 RegWr)所需要的控制信号。
隔离保护 :它确保当 EX 阶段开始处理下一条新指令时,当前指令算出的结果已经安全地存在 EX/MEM 里,供 MEM 阶段慢慢使用。
一、 转发路径总结 (Forwarding Paths) 转发技术通过在硬件中增加多路选择器(MUX)和反馈连线,使 ALU 能够直接从流水线后级寄存器中获取尚未写回的数据。
路径名称
数据来源
数据去向
解决的冲突类型
EX 级转发 (C1)
EX/MEM 流水线寄存器
ALU 的输入端 MUX
相邻指令 间的 RAW 冒险(如 add 紧跟 sub)
MEM 级转发 (C2)
MEM/WB 流水线寄存器
ALU 的输入端 MUX
间隔一条指令 间的 RAW 冒险
二、 转发条件总结 (Forwarding Conditions) 为了确保转发的准确性,硬件判定逻辑必须满足“三看”:一看写使能,二看 Rd 是否为 0,三看寄存器编号匹配。
1. EX 级转发条件 (相邻指令) 当满足以下条件时,开启从 EX/MEM 到当前指令 EX 阶段的转发:
C1(a) 转发 Rs: EX/MEM.RegWr and EX/MEM.RegisterRd != 0 and EX/MEM.RegisterRd == ID/EX.RegisterRs
C1(b) 转发 Rt: EX/MEM.RegWr and EX/MEM.RegisterRd != 0 and EX/MEM.RegisterRd == ID/EX.RegisterRt
2. MEM 级转发条件 (间隔指令 + 优先级修正) 为了处理如 image_dfe02b.png 中连续多条指令写同一个寄存器的复杂情况,必须保证“最新数据优先” 。因此,只有在 EX 级不需要转发时,才考虑 MEM 级转发。
以 Rs 为例,完善后的 C2(a) 条件为:
基础匹配: MEM/WB.RegWr and MEM/WB.RegisterRd != 0 and MEM/WB.RegisterRd == ID/EX.RegisterRs
优先级限制(关键): and NOT (EX/MEM.RegWr == 1 and EX/MEM.RegisterRd != 0 and EX/MEM.RegisterRd == ID/EX.RegisterRs)
三、 转发技术的特殊限制 即使拥有完善的转发逻辑,仍有两种情况需要注意:
Load-use 冒险: 当 lw 指令紧跟一条需要该数据的指令时,由于数据在物理上直到 MEM 阶段结束才产生,无法直接转发,必须配合阻塞(Stall)一个周期 。
无需写回的指令: 如 beq 或 sw,由于其 RegWr 信号为 0,即便寄存器编号匹配也不会触发转发。
Load-use Data Hazard(硬件阻塞方式)
1. 为什么 Load-use 必须阻塞? 在 MIPS 五级流水线中,lw 指令产出的数据非常晚。
数据就绪点 :lw 指令直到 Mem(访存) 阶段结束(即第四时钟周期末)才拿到内存数据。
数据使用点 :紧随其后的指令(如 sub)在自己的 Exec(执行) 阶段(即第四周期初)就需要数据进行 ALU 运算。
物理瓶颈 :由于使用点早于生产点,即便有转发技术也无法“逆转时间”完成数据传递。因此,硬件必须介入,强制将后续指令推后一个周期。
2. 硬件如何检测阻塞? 硬件中有一个专门的 “冒险检测单元” 。它在 ID(译码) 阶段通过以下逻辑判断是否需要阻塞:
判定条件:
上一条指令是 Load :即 ID/EX.MemRead == 1。
目标寄存器冲突 :上一条 Load 的目的寄存器(ID/EX.RegisterRt)等于当前正在译码指令的源寄存器(IF/ID.RegisterRs 或 IF/ID.RegisterRt)。
3. 阻塞的具体实现动作 一旦检测到 Load-use 冒险,硬件会执行以下三步操作来制造一个“气泡(Bubble)”:
插入气泡 (清除 ID/EX 寄存器) : 将 ID/EX 段寄存器中所有的控制信号清零(相当于执行了一条 nop 指令)。这样,原本该进入执行阶段的指令就不会对寄存器或内存产生任何修改。
冻结 IF/ID 寄存器 : 保持 IF/ID 寄存器中的信息不变。这意味着当前被阻塞的指令(如 sub)会被留在译码阶段,并在下一个周期重新译码执行。
冻结 PC 寄存器 : 保持 PC 的值不变。这确保了再下一条指令(如 and)在下一个周期会被重新取出,而不会因为 PC 增加而跳过。
方案5:编译器进行指令顺序调整来解决数据冒险
1. 为什么需要编译器优化? 虽然硬件可以自动阻塞(Stall),但阻塞意味着 CPU 在“原地踏步”,会白白浪费时钟周期。
硬件方案 :发现冲突 -> 停顿 1 周期 -> 总耗时变长。
编译器方案 :调整顺序 -> 消除冲突 -> 0 停顿 ,满速运行。
2. 实战演练:从 Slow Code 到 Fast Code 参考 image_e126bb 中的代码逻辑:
目标计算:a = b + c; 和 d = e - f;
❌ 优化前 (Slow Code):频繁阻塞 Plaintext
1 2 3 4 5 6 7 8 lw $2, b lw $3, c <-- 紧接着要用 $3 add $1, $2, $3 <-- [冒险!] 必须在这里阻塞 1 周期 sw $1, a lw $5, e lw $6, f <-- 紧接着要用 $6 sub $4, $5, $6 <-- [冒险!] 必须再次阻塞 1 周期 sw $4, d
分析 :这段代码会触发两次硬件阻塞,总共浪费 2 个周期。
✅ 优化后 (Fast Code):无缝衔接 编译器通过观察发现,a=b+c 和 d=e-f 是互不干涉的两组运算。它将指令重新排序:
MIPS Assembler
1 2 3 4 5 6 7 8 lw $2, b lw $3, c lw $5, e # [巧妙插入] 在等待 c 加载时,先去加载 e add $1, $2, $3 # [冒险消除!] 此时 c 早已加载完毕,直接计算 lw $6, f # [巧妙插入] 在等待 e 加载时,先去加载 f sw $1, a # [无关指令] 进一步拉开 lw $6 和 sub 的距离 sub $4, $5, $6 # [冒险消除!] 此时 f 也加载好了,直接计算 sw $4, d
分析 :调整后,Load 指令和它对应的运算指令之间都被“拉开了距离”。通过这种方式,硬件不再需要任何阻塞,CPU 始终保持满载 。
3. 编译器优化的“底层逻辑” 我们可以用 ASCII 时序图看看这个“拉开距离”的效果:
1 2 3 4 5 6 7 8 优化前 (有阻塞): lw $3: [IF][ID][EX][MEM][WB] add: [IF][ID]----[ID][EX] <-- 被迫停顿 (Stall) 优化后 (无阻塞): lw $3: [IF][ID][EX][MEM][WB] lw $5: [IF][ID][EX][MEM][WB] <-- 利用这个周期干别的活 add: [IF][ID][EX][MEM][WB] <-- 等到这里用 $3 时,数据早已出炉!
控制冒险
1. 什么是控制冒险? 控制冒险,也称为分支冒险 。它的核心矛盾在于:处理器在还没确定下一条指令该去哪取的时候,就已经开始取指了 。
在 MIPS 五级流水线中,以 Beq(相等则转移)指令为例:
判定延迟 :指令是否转移是在 Mem(访存) 阶段确定的。
结果 :如图 image_e2f459 所示,当 Beq 到达第七周期确定跳转地址并送往 PC 时,流水线已经默认按顺序取出了后面 3 条 错误的指令。
代价 :如果发生转移,这 3 条已经进入流水线的指令必须被清除(Flush) ,这导致了 3 个时钟周期的延迟损失(C=3) 。
2. 解决控制冒险的 4 种方法 针对这种性能损失,PPT 提出了四种主要的解决方案:
方法 1:硬件阻塞(Stall)
做法 :一旦检测到分支指令,强行让流水线停顿,直到分支结果确定。
缺点 :每遇到分支就插入 3 条 NOP 指令,效率极低。
方法 2:软件插入 NOP
做法 :由编译器在分支指令后手动加入三条无关的 NOP 指令。
评价 :与硬件阻塞类似,虽然保证了正确性,但浪费了大量周期。
方法 3:分支预测(Branch Prediction) 这是现代处理器最常用的高效手段:
静态预测 :总是预测“不发生转移”(继续执行后续指令)。如果预测错了,再清空流水线重新开始。
动态预测 :根据程序执行的历史记录(如循环统计)实时调整预测策略,准确率可高达 90% 。
方法 4:延迟分支(Delayed Branch)
做法 :编译器寻找一条与分支无关的指令,将其移动到分支指令之后执行。
效果 :无论分支是否成功,这条处于“延迟槽”中的指令都会执行,从而利用了原本会被浪费掉的一个时钟周期。
简单(静态)分支预测方法 1. 静态预测的主要策略:总是预测“不发生”(Predict Not Taken) 这是 MIPS 流水线中最常用、最简单的静态预测方法。
做法 :当流水线遇到一条分支指令(如 beq)时,硬件默认认为分支不会跳转 ,于是继续按照 PC+4 的地址去取下一条指令。
预测成功 :如果 beq 的条件真的不成立(不跳转),流水线就完美地避开了停顿,实现零延迟 。
预测失败 :如果 beq 的条件成立(需要跳转),那么之前已经取进来的指令就是错误的,必须被清空(Flush) 。
2. 预测失败时的“清空”过程(ASCII 时序图) 假设 beq 发生了跳转,但我们预测它“不跳转”:
Plaintext
1 2 3 4 5 6 周期: C1 C2 C3 C4 C5 beq: [IF] [ID] [EX] [MEM] <-- 在MEM阶段发现:wc!预测错了,要跳转 instr+1: [IF] [ID] [EX] [清空] <-- 错误取到的指令,必须作废(变气泡) instr+2: [IF] [ID] [清空] <-- 错误取到的指令,作废 instr+3: [IF] [清空] <-- 错误取到的指令,作废 target: [IF] <-- 第5周期才真正取到正确的目标指令
代价 :在基础流水线中,预测失败会造成 3 个时钟周期 的损失。
3. 如何优化预测失败的代价? 正如 image_e2f8f9.png 所提到的,为了让静态预测更划算,硬件会把“分支判定”的时机提前到 ID 阶段 。
硬件改动 :在 ID 阶段增加专门的比较器和加法器。
优化后的效果 :
预测失败只需清空 1 条 指令(即正在 IF 阶段的那条)。
代价 :损失从 3 周期降到了 1 周期 。
动态分支预测方法 动态分支预测的核心思想是:利用分支指令最近的执行历史,来预测下一次是否会发生转移 。它不像静态预测那样死板,而是会根据程序的实际运行情况动态调整预测结果。
1. 核心组件:分支历史表 (BHT) 动态预测主要依靠一个特殊的硬件结构——BHT (Branch History Table) ,也常被称为 BPB (Branch Prediction Buffer) 。
工作流程 :
查找 :在 IF (取指) 阶段,利用分支指令地址的低位作为索引去查找 BHT。
预测 :从表中读出“预测位”,决定是“跳转取指”还是“顺序取指”。
修正 :指令实际执行完后,将真实的跳转结果反馈给 BHT,更新该表项的预测位。
2. 预测算法:从 1 位到 2 位 A. 1 位预测位 (1-bit Predictor)
逻辑 :总是按上一次实际发生的情况来预测下一次。
1 表示最近一次发生了转移,下次预测跳转 (taken)。
0 表示最近一次没发生转移,下次预测顺序执行 (not taken)。
缺点 :在循环边界(第一次和最后一次)会产生两次连续的预测错误,因为循环的状态改变得太快,1 位逻辑反应不过来。
B. 2 位预测位 (2-bit Predictor)——主流方案 为了提高准确率,现代处理器(如 Pentium 4)多采用 2 位或更多位的预测位。
逻辑 :用 2 位组合四种情况来表示预测状态(如“强跳转”、“弱跳转”、“弱不跳转”、“强不跳转”)。
优点 :只有在连续两次 分支情况改变时,才会改变预测方向。这使得它在处理循环时,只会产生一次预测错误。
一位动态预测 1. 一位预测的核心逻辑:惯性思维 一位预测器的核心思想是:“上次发生什么,这次就猜什么” 。它只有两个状态,由 1 位二进制数表示:
状态 1(预测发生/Taken) :如果上次分支跳转了,状态变为 1。下次遇到该指令,默认选择“跳转取指”。
状态 0(预测不发生/Not Taken) :如果上次分支没跳转,状态变为 0。下次遇到该指令,默认选择“顺序取指”。
2. 状态转换图详解 结合 image_e3705c.png 中的圆圈和箭头,我们可以看到状态是如何随着实际执行结果而“反转”的:
预测正确时 :状态保持不变。例如,当前在“状态 1”,实际执行结果也是“发生”,则继续留在“状态 1”。
预测错误时 :状态发生反转。
如果在“状态 1”但实际“不发生”,状态立刻切换到 0。
如果在“状态 0”但实际“发生”,状态立刻切换到 1。
3. 致命弱点:循环边界的“连错两次” 一位预测器最典型的局限性体现在循环程序中。假设有一个执行了 10 次的循环:
退出循环时(第 10 次) :之前一直是跳转的(状态 1),但最后一次不再跳转。此时预测器猜错第一次 ,并把状态改为 0。
再次进入循环时(下一次外层迭代) :因为状态变成了 0,它会预测“不跳转”。但实际上循环的第一步通常是要跳转的。此时预测器又猜错第二次 ,再把状态改回 1。
结论 :只要本次执行和上次的情况不同,就会出现一次预测错误。在嵌套循环中,内层循环每结束一次,都会导致下一次进入时多错一次。
4. 实例计算:双重循环的准确率 参考 image_e37076.png 的例子:
外循环执行 $N$ 次,内循环执行 $N$ 次 。
预测错误总数 :$1 + 2 \times (N-1)$ 次。
第一次内循环结束错 1 次。
后面每次重新进入内循环(第一步)错 1 次,结束时又错 1 次。
趋势 :当 $N=10$ 时,准确率约 90.9% ;当 $N=100$ 时,准确率提高到 99% 。这意味着 $N$ 越大,预测准确率越高 ,因为中间正确预测的次数显著增多。
两位动态预测 1. 两位预测状态机 (2-bit State Machine) 结合 image_e37fa0.png,两位预测器由四个状态组成,可以看作是一个有“缓冲带”的决策系统:
状态码
含义
预测动作
容错性
11
强预测发生 (Strongly Taken)
预测跳转
错一次后降级为 10,但下次依然猜跳转。
10
弱预测发生 (Weakly Taken)
预测跳转
再错一次才彻底放弃,转为 01。
01
弱预测不发生 (Weakly Not Taken)
预测顺序
再错一次(即实际发生)才转为 10。
00
强预测不发生 (Strongly Not Taken)
预测顺序
错一次后升温为 01,下次依然猜顺序。
2. 为什么它比一位预测更强? 它的绝活在于处理循环边界 。我们用 ASCII 时序图来对比它们处理同一个循环(循环 10 次)的表现:
一位预测器 :
退出循环时 :猜错第 1 次,状态变 0。
下次进入循环时 :根据状态 0 猜不跳,结果跳了,猜错第 2 次 。
两位预测器 (初始为 11) :
退出循环时 :猜错第 1 次,状态从 11 降级到 10 (弱预测发生)。
下次进入循环时 :由于状态是 10,它依然预测“跳转” 。结果真的跳了,预测正确! 状态升回 11。
结论 :两位预测器成功地通过“缓冲状态”过滤掉了循环结束时的那次偶然失误,保证了下次进入循环时的首跳准确。
3. 实例计算:双重循环的准确率
内循环每次结束都会预测错误,一共有n次,外循环结束还有一次,一共是n+1次
分支延迟时间片的调度 根据您提供的资料(特别是 image_edec08.png),我为您详细讲解分支延迟时间片的调度 。
这是一种通过编译器 重排指令顺序来消除控制冒险的静态调度技术。
1. 核心概念:分支延迟槽 (Branch Delay Slot) 在流水线中,分支指令确定跳转方向和目标地址需要时间。在结果确定之前,流水线已经取出的指令位置被称为分支延迟时间片 (或分支延迟槽 )。
基本思想 :与其在延迟槽中插入浪费周期的 nop,不如让编译器找一条在分支指令之前、且与分支结果无关的指令,将其移动到分支指令后面 执行。
特性 :处于延迟槽中的指令,无论分支是否发生转移,都一定会被执行 。
2. 实例解析:如何进行调度 参考 image_edec08.png 中的代码优化过程:
优化前(有浪费): 原本的代码顺序如下,假设分支判定提前到了 ID 阶段,延迟时间片为 1:
MIPS Assembler
1 2 3 4 5 6 7 lw $1, 0($2) lw $3, 0($2) add $6, $4, $2 beq $3, $5, 2 # 分支指令 nop # 延迟槽:如果不调度,这里必须放一个空操作 add $3, $3, $2 sw $1, 0($2)
优化后(满速运行): 编译器发现 lw $1, 0($2) 这条指令与分支指令 beq $3, $5, 2 完全无关,因此将其“挪”到了分支指令之后:
MIPS Assembler
1 2 3 4 5 6 lw $3, 0($2) add $6, $4, $2 beq $3, $5, 2 # 分支指令 lw $1, 0($2) # 【调度后】这条指令填补了延迟槽,不再需要 nop! add $3, $3, $2 sw $1, 0($2)
效果 :原本会浪费的一个时钟周期被有效指令填满,流水线效率大幅提升。
异常(Exception)和中断(Interrupt) 异常(Exception)和中断(Interrupt)是另一种形式的控制冒险 ,因为它们会改变程序的正常执行流程。
1. 异常的发生与检测 当流水线中的某条指令发现异常时(例如 ALU 指令发现“溢出”),后面的多条指令可能已经进入流水线并在执行中。
实例 :指令 add r1, r2, r3 产生溢出。
检测点 :溢出通常在 EXE(执行)阶段 被检测出来。此时,该指令后面的两条指令已经分别进入了 ID 和 IF 阶段。
2. 流水线处理异常的步骤 为了保证程序的正确性,硬件必须确保发生异常的指令及其后续指令不会对寄存器或内存造成永久性修改。
清除指令 (Flush) :
EX.Flush :将 EXE 阶段指令的控制信号清 0(特别是 RegWr),避免将错误的溢出结果写回寄存器。
ID.Flush :将 IF/ID 寄存器中的指令清 0,转变为 nop 指令。
IF.Flush :清除正在取指阶段的指令。
关中断 :将中断允许触发器清 0。
保存断点 :将当前的 PC 或 PC-4 保存到 EPC (Exception Program Counter) 寄存器中,以便之后能返回执行。
跳转到处理程序 :将 MIPS 异常处理程序的首地址 0x8000 0180 送入 PC,从该地址开始取指执行。
3. 处理过程图解 (ASCII) 当 add 指令在 EXE 阶段报错时,流水线会瞬间插入多个“气泡(bubble)”:
Plaintext
1 2 3 4 5 周期: C1 C2 C3(报错!) C4 C5 add: [IF] [ID] [EXE] ----> [bubble] [bubble] (防止写回r1) sub: [IF] [ID] ----> [bubble] [bubble] (被冲刷) and: [IF] ----> [bubble] [bubble] (被冲刷) Handler: [0x8000 0180] (异常处理开始)
参考资料 「小白debug」如何用开关造出一台计算机_哔哩哔哩_bilibili