定点数的移位运算
移位在硬件上是最便宜的运算——它不需要加法器,不需要进位链,甚至不需要门电路,只是把线接到旁边一位上。正因为便宜,它被大量用来替代乘除法,也因此成了本章陷阱最密集的一节。
三类移位的区别,说到底只有两个问题:移空的那一位补什么,移出去的那一位去哪。 把这两问答清楚,三类移位就再也不会混。
疑问点:逻辑移位、算术移位、循环移位的区分标准是什么
常见的记忆是”逻辑移位对应
shl,算术移位要保持正负、空位补 0 或 1,循环移位对应rol/ror”。该记忆的方向正确,但需要补全两点:算术移位补什么取决于机器码是原码、补码还是反码;循环移位还要再分带不带进位位。
机制
两个判据
| 空位补什么 | 移出的位去哪 | |
|---|---|---|
| 逻辑移位 | 一律补 0 | 丢弃(或存入 CF) |
| 算术移位 | 补什么由符号和机器码决定 | 丢弃(或存入 CF) |
| 循环移位(小循环) | 补移出去的那一位 | 绕回另一端 |
| 循环移位(大循环) | 补 CF 的原值 | 存入 CF |
“补 0 还是补符号”是逻辑与算术的分水岭;“移出的位丢不丢”是循环与非循环的分水岭。 两个判据正交,四种组合都存在。
逻辑移位:把数当无符号位串
逻辑移位不承认符号位的存在,整个字长就是一串普通的位。左移右移都在空出的位补 0。
它对应无符号数的乘 2 / 除 2。用在有符号数上会立刻出错:1111 1111(0111 1111(
算术移位:保持真值的符号与大小关系
算术移位要保证移位后的位串在同一种编码下仍表示”原值乘/除 2”。符号位始终不参与移动(补码的符号位在右移时要被复制,这是”不动”的另一种说法)。
补什么,由机器码决定:
| 机器码 | 正数 | 负数(左移) | 负数(右移) |
|---|---|---|---|
| 原码 | 补 0 | 补 0 | 补 0 |
| 补码 | 补 0 | 补 0 | 补 1(即符号位) |
| 反码 | 补 0 | 补 1 | 补 1 |
这张表可以压成一句话:除了”补码负数右移”和”反码负数”之外,全都补 0。
为什么补码负数右移要补 1,可以用权值解释直接验证:1111 1000,右移一位补 1 得 1111 1100 0111 1100
循环移位:位不丢
循环移位不改变位串中 1 的个数,只是把它们整体转了个圈。它不对应任何乘除运算,主要用于位段重排和多字长运算的位传递。
小循环(不带进位位):移出的位直接补到另一端。
大循环的用途很具体:多字长数据的移位。把 64 位数存在两个 32 位寄存器里做右移时,高 32 位先移、移出的位落进 CF,低 32 位再用带进位的循环移位把 CF 接进来。如果没有 CF 参与,两个寄存器之间的那一位就会丢失。
移位与乘除 2 的对应,以及它的裂缝
| 操作 | 对应运算 | 例外 |
|---|---|---|
| 算术左移 1 位 | 可能溢出 | |
| 算术右移 1 位 | 负数的取整方向与 C 的除法不同 |
左移的溢出判据:算术左移后,若移出的那一位与新的符号位不同,则溢出。8 位补码 0100 0000(1000 0000(
右移的裂缝更隐蔽,且是高频考点:
算术右移向负无穷取整(下取整),C 的整数除法向零取整(截断)。 两者在被除数为负且不能整除时相差 1。
因此”编译器会把 x / 2 优化成 x >> 1”这句话只对无符号数和非负数成立。对有符号数,编译器实际生成的是”先加偏移再移位”:(x + (x >> 31 & 1)) >> 1,靠符号位造出一个
移位由谁实现
层次辨析:移位功能的硬件归属没有唯一答案,做题看图
一种常见的预期是”算术移位和循环移位需要与 ALU 配合才能完成”。实际情况是:三类移位都只需要移位部件本身,不需要 ALU 参与;而这个移位部件既可以集成在 ALU 内部由
选择,也可以作为 ALU 之外的独立移位器 / 移位寄存器由 控制。两种实现在真实电路中都存在,教材图与实验板电路不一致属于正常现象。
三种实现方式:
| 实现 | 控制信号 | 特点 |
|---|---|---|
| 集成在 ALU 内 | 部件少,ALU 的功能选择逻辑变复杂 | |
| 独立移位器 / 移位寄存器(SR) | 单独的 | 职责清晰,数据通路图上单独画一个框 |
| 桶形移位器(barrel shifter) | 移位量作为输入 | 一个周期内移任意位数,真实 CPU 的做法 |
普通移位寄存器一次只能移一位,移
数据通路题的判据只有一条:看图上有没有单独的移位部件。 图里画了 SR 且题目给了
C 语言的移位语义
x << k /* 左移 k 位,低位补 0 */
x >> k /* 右移 k 位,补什么取决于 x 的类型 */x 的类型 | x >> k |
|---|---|
| 无符号 | 逻辑右移,补 0(标准强制) |
| 有符号 | 实现定义;实际上几乎所有编译器都用算术右移 |
三个未定义行为,考试会问:
- 移位量
类型宽度:int x; x << 32是 UB。硬件上 x86 会把移位量对 32 取模(x << 32等于x << 0,即不变),但这不是语言承诺。 - 移位量为负:UB。
- 有符号数左移导致溢出:UB。
还有一个容易忽略的点:移位的两个操作数各自独立做整数提升,但不做通常算术转换。所以 char c; c << 1 的结果类型是 int,不会截断到 8 位。
边界
逻辑右移 vs 算术右移
判据是”高位补 0 还是补符号位”,而不是”数据本身是正是负”。
同一串位 1111 1111:逻辑右移得 0111 1111(127),算术右移得 1111 1111(
考题里若只说”右移”而不指明,必须先看题干给的数是什么编码、什么类型,不能默认。
算术左移 vs 逻辑左移:位串相同,判溢出不同
左移时两者补的都是 0,位串结果完全一样。 区别只在是否判溢出、按什么判:
- 逻辑左移:按无符号看,移出的位就是 CF。
- 算术左移:按有符号看,移出位与新符号位不同即溢出。
所以”算术左移和逻辑左移是同一个操作”这句话,在位串层面成立,在溢出判定层面不成立。
循环移位不改变 1 的个数
这是判断题的常客:逻辑移位和算术移位会改变位串中 1 的个数(有位被丢弃或补入),循环移位不会(小循环严格不变;大循环把 CF 算进去也不变)。
由此还可推出:循环移位不对应乘除 2,也不产生溢出。
移位不能替代所有乘除
这条思路正是 2.2.4 移位-加法乘法的雏形:乘法的本质就是”若干次移位后相加”。
对照速查
| 左移补 | 右移补 | 移出位 | |
|---|---|---|---|
| 逻辑移位 | 0 | 0 | 丢弃 / CF |
| 算术移位(补码) | 0 | 符号位 | 丢弃 / CF |
| 算术移位(原码) | 0 | 0(符号位不动) | 丢弃 |
| 算术移位(反码负数) | 1 | 1 | 丢弃 |
| 小循环 | 移出位 | 移出位 | 绕回另一端 |
| 大循环 | CF 原值 | CF 原值 | 进 CF |
| 判据 | 结论 |
|---|---|
| 算术左移溢出 | 移出位 |
| 算术右移取整方向 | 向负无穷(C 的 / 是向零) |
x >> 1 能否代替 x / 2 | 无符号可以;有符号在 |
| 移位改变 1 的个数吗 | 循环移位不改变,其余会 |
考点
- 三类移位的判据:空位补什么 + 移出位去哪
- 补码负数右移补 1(补符号位),原码负数右移补 0
- 反码负数移位一律补 1
- 大循环带 CF,周期为
,用于多字长移位 - 算术左移溢出判据:移出位
新符号位 - 算术右移向负无穷取整,C 的整数除法向零截断,负数不整除时差 1
- 移位部件可在 ALU 内也可在 ALU 外;桶形移位器一周期移任意位
- C 中无符号右移必为逻辑右移,有符号右移是实现定义
- 移位量
宽度是未定义行为 - 循环移位不改变 1 的个数,也不产生溢出
链接
- 🏠 返回总览:计算机组成原理第 2 章:数据的表示和运算总览
- ⬅️ 上一节:2.2.1 基本运算部件
- ➡️ 下一节:2.2.3 定点数的加减运算
- 📖 名词库:第 2 章名词库
- 📜 原始提问:第 2 章原始提问档案(本地资料)