定点数的移位运算

移位在硬件上是最便宜的运算——它不需要加法器,不需要进位链,甚至不需要门电路,只是把线接到旁边一位上。正因为便宜,它被大量用来替代乘除法,也因此成了本章陷阱最密集的一节。

三类移位的区别,说到底只有两个问题:移空的那一位补什么,移出去的那一位去哪。 把这两问答清楚,三类移位就再也不会混。

疑问点:逻辑移位、算术移位、循环移位的区分标准是什么

常见的记忆是”逻辑移位对应 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,可以用权值解释直接验证: 的 8 位补码是 1111 1000,右移一位补 1 得 1111 1100 ,正确;若补 0 得 0111 1100 ,完全错误。补符号位是唯一能保持数值除以 2 的补法——与符号扩展是同一个道理的两次出现。

循环移位:位不丢

循环移位不改变位串中 1 的个数,只是把它们整体转了个圈。它不对应任何乘除运算,主要用于位段重排和多字长运算的位传递。

小循环(不带进位位):移出的位直接补到另一端。循环左移位大循环(带进位位):把 CF 当成位串的第 位,一起参与循环。移出的位进 CF,CF 的原值补到另一端。大循环的周期是 而不是 。

大循环的用途很具体:多字长数据的移位。把 64 位数存在两个 32 位寄存器里做右移时,高 32 位先移、移出的位落进 CF,低 32 位再用带进位的循环移位把 CF 接进来。如果没有 CF 参与,两个寄存器之间的那一位就会丢失。

移位与乘除 2 的对应,以及它的裂缝

操作对应运算例外
算术左移 1 位可能溢出
算术右移 1 位负数的取整方向与 C 的除法不同

左移的溢出判据:算术左移后,若移出的那一位与新的符号位不同,则溢出。8 位补码 0100 0000()左移得 1000 0000(),移出的是 0、新符号位是 1,判定溢出——确实, 超出了 8 位补码范围。

右移的裂缝更隐蔽,且是高频考点:

而中

算术右移向负无穷取整(下取整),C 的整数除法向零取整(截断)。 两者在被除数为负且不能整除时相差 1。

因此”编译器会把 x / 2 优化成 x >> 1”这句话只对无符号数和非负数成立。对有符号数,编译器实际生成的是”先加偏移再移位”:(x + (x >> 31 & 1)) >> 1,靠符号位造出一个 的修正。知道这一点,就能解释为什么有符号除法比无符号除法慢一点。 详见 2.2.4。

移位由谁实现

层次辨析:移位功能的硬件归属没有唯一答案,做题看图

一种常见的预期是”算术移位和循环移位需要与 ALU 配合才能完成”。实际情况是:三类移位都只需要移位部件本身,不需要 ALU 参与;而这个移位部件既可以集成在 ALU 内部由 选择,也可以作为 ALU 之外的独立移位器 / 移位寄存器由 控制。两种实现在真实电路中都存在,教材图与实验板电路不一致属于正常现象。

三种实现方式:

实现控制信号特点
集成在 ALU 内 的一个取值部件少,ALU 的功能选择逻辑变复杂
独立移位器 / 移位寄存器(SR)单独的 职责清晰,数据通路图上单独画一个框
桶形移位器(barrel shifter)移位量作为输入一个周期内移任意位数,真实 CPU 的做法

普通移位寄存器一次只能移一位,移 位要 个周期;桶形移位器用多级 MUX 实现, 级即可完成任意位数的移位,代价是面积。这又是一次”面积换时间”,与 先行进位加法器的取舍完全同构。

数据通路题的判据只有一条:看图上有没有单独的移位部件。 图里画了 SR 且题目给了 ,移位就由 SR 做;图里只有 ALU,移位就是 ALU 的一种功能。两种画法的答案不同,但都不算错。

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()。是编译器根据变量的类型选择了指令,硬件本身两条指令都提供。 这是 2.1.3 五处区分中的第三处。

考题里若只说”右移”而不指明,必须先看题干给的数是什么编码、什么类型,不能默认。

算术左移 vs 逻辑左移:位串相同,判溢出不同

左移时两者补的都是 0,位串结果完全一样。 区别只在是否判溢出、按什么判:

  • 逻辑左移:按无符号看,移出的位就是 CF。
  • 算术左移:按有符号看,移出位与新符号位不同即溢出。

所以”算术左移和逻辑左移是同一个操作”这句话,在位串层面成立,在溢出判定层面不成立。

循环移位不改变 1 的个数

这是判断题的常客:逻辑移位和算术移位会改变位串中 1 的个数(有位被丢弃或补入),循环移位不会(小循环严格不变;大循环把 CF 算进去也不变)。

由此还可推出:循环移位不对应乘除 2,也不产生溢出。

移位不能替代所有乘除

和 可以用移位,其他常数不行——但可以用移位 + 加减组合:,。编译器普遍这么做,因为移位和加法都是一个周期,乘法器要多个周期。

这条思路正是 2.2.4 移位-加法乘法的雏形:乘法的本质就是”若干次移位后相加”。

对照速查

左移补右移补移出位
逻辑移位00丢弃 / CF
算术移位(补码)0符号位丢弃 / CF
算术移位(原码)00(符号位不动)丢弃
算术移位(反码负数)11丢弃
小循环移出位移出位绕回另一端
大循环CF 原值CF 原值进 CF
判据结论
算术左移溢出移出位 新符号位
算术右移取整方向向负无穷(C 的 / 是向零)
x >> 1 能否代替 x / 2无符号可以;有符号在 且不整除时差 1
移位改变 1 的个数吗循环移位不改变,其余会

考点

  • 三类移位的判据:空位补什么 + 移出位去哪
  • 补码负数右移补 1(补符号位),原码负数右移补 0
  • 反码负数移位一律补 1
  • 大循环带 CF,周期为 ,用于多字长移位
  • 算术左移溢出判据:移出位 新符号位
  • 算术右移向负无穷取整,C 的整数除法向零截断,负数不整除时差 1
  • 移位部件可在 ALU 内也可在 ALU 外;桶形移位器一周期移任意位
  • C 中无符号右移必为逻辑右移,有符号右移是实现定义
  • 移位量 宽度是未定义行为
  • 循环移位不改变 1 的个数,也不产生溢出

链接