try ai
科普
编辑
分享
反馈
  • 逻辑移位与算术移位

逻辑移位与算术移位

SciencePedia玻尔百科
核心要点
  • 主要区别在于它们如何填充空位:逻辑移位总是使用零,而算术右移则复制原始的符号位。
  • 逻辑移位将数字视为简单的位模式(无符号),而算术移位旨在保留有符号数的数学值。
  • 算术右移 kkk 位等同于向下取整的除法(除以 2k2^k2k),这对于有符号整数数学至关重要,但可能需要校正以匹配特定语言的取整规则。
  • 这一区别对于硬件设计(移位器)、编译器优化(强度削减)以及符号扩展等基本操作至关重要。

引言

位移位是计算机处理器可以执行的最基本、最高效的操作之一,作为一种通过 2 的幂进行乘除法的高速方法。然而,将位向左或向右滑动的简单动作揭示了一个关键的设计选择:新空出的位位置应该用什么值来填充?这个问题的答案并非微不足道的细节;它是一个基础概念,将位操作的世界一分为二,构成了逻辑移位和算术移位之间的核心区别。这一选择直接影响数值数据的完整性,尤其是在处理无符号模式和有符号数之间的差异时。

本文深入探讨了这一本质区别。我们将首先探索定义逻辑移位和算术移位的核心原理和机制,揭示一种如何为原始位模式量身定制,而另一种又是如何巧妙地设计来保留有符号整数的数学属性。随后,我们将审视这些操作的深远应用和跨学科联系,从 CPU 硬件的设计和软件编译器的巧妙优化,到在抽象的音乐理论世界中令人惊讶的相似之处。

原理与机制

在计算机处理器的核心,于一切复杂性之中,存在着一些简单得惊人却又功能强大的操作。其中最基本的当属​​位移位​​。对一个数进行移位,本质上就是将其二进制数字在其容器——寄存器——内向左或向右滑动。如果你有数字 8,其二进制表示为 00001000,向左移一位得到 00010000,即数字 16。向右移一位得到 00000100,即数字 4。这似乎是一种执行乘以或除以 2 的幂的绝妙快捷方式。

但这个简单的滑动动作立即让我们面临一个深刻的问题:当我们移动数字时,会产生一个空位。我们用什么来填充它?这个问题的答案不仅仅是一个技术细节;它将移位的世界一分为二,创造了一个对计算机如何处理数据至关重要的区别。这一选择为我们带来了两种主要的移位类型:​​逻辑移位​​和​​算术移位​​。

逻辑世界:作为模式的位

对我们问题的最简单回答是始终用零填充空位。这就是​​逻辑移位​​。​​逻辑左移​​将位向左滑动,并用零填充右侧的空位。​​逻辑右移​​将位向右滑动,并用零填充左侧的空位。

这种方法因其一致性而显得优美。它不把数字看作具有正负值的数学量,而仅仅看作一种位模式。这种“无符号”的观点对于许多任务来说是完美的。当计算机处理文本时,每个字符都由一个数字(如 ASCII 或 Unicode 值)表示,这仅仅是一个代码。它没有正负之分;它就是它本身。对于这些无符号量,逻辑移位完全符合我们的直觉:向左移 kkk 位将数字乘以 2k2^k2k,向右移 kkk 位则执行整数除法(除以 2k2^k2k)。它干净、简单且“合乎逻辑”。

但是,当我们的位模式要表示可正可负的有符号数时,会发生什么呢?在这里,逻辑移位优雅的简洁性会导致数学上的混乱。

算术挑战:保留符号

现代计算机绝大多数使用一种称为​​补码​​的巧妙方案来表示有符号整数。在这个系统中,最高有效位(最左边的位)充当​​符号位​​。如果它是 000,则该数为非负数。如果它是 111,则该数为负数。对于一个 8 位数,这个符号位不仅代表一个符号;它还有一个 −128-128−128 的数值权重。例如,数字 −3-3−3 表示为 11111101,这对应于 −128+64+32+16+8+4+1=−3-128 + 64 + 32 + 16 + 8 + 4 + 1 = -3−128+64+32+16+8+4+1=−3。

现在,让我们尝试用逻辑右移来计算 −3-3−3 除以 222。我们将 11111101 右移一位,并在左侧的空位填充一个 000。结果是 01111110。符号位现在是 000,所以这是一个正数。它的值是 64+32+16+8+4+2=12664 + 32 + 16 + 8 + 4 + 2 = 12664+32+16+8+4+2=126。我们从 −3-3−3 开始,试图除以 222,结果却得到了 126126126。这完全是无稽之谈。逻辑移位通过插入一个零,破坏了我们数字的符号。

这就是​​算术移位​​发挥作用的地方。它的设计只有一个目的:在移位过程中保持有符号数的数学完整性。它的规则与逻辑移位一样简单,但有着深刻的不同:

  • ​​算术左移​​与逻辑左移相同。(远离符号位的移动不会引起问题)。
  • ​​算术右移​​通过复制原始符号位来填充左侧的空位。

让我们重新尝试用 222 来除 −3-3−3(11111101)。符号位是 111。我们将位向右移动一位,并用另一个 111 填充左侧的新空间。结果是 11111110。在补码中,这表示值 −2-2−2。对于 −3-3−3 除以 222 的除法来说,这是一个完全合理的结果。

算术移位的普适真理

让我们更仔细地看看那个结果。为什么是 −2-2−2?如果我们用计算器进行除法,−3/2=−1.5-3 / 2 = -1.5−3/2=−1.5。在整数的世界里,我们必须对这个结果进行取整。有些人可能会取整到 −1-1−1(向零取整),而另一些人可能会取整到 −2-2−2(向负无穷大取整)。算术移位选择了 −2-2−2。

让我们再试一个。−1/2-1 / 2−1/2 怎么样?在 8 位中,−1-1−1 是 11111111。算术右移复制符号位,所以结果是... 11111111,仍然是 −1-1−1。

这是怎么回事?向负无穷大取整的数学运算称为​​向下取整​​(floor)函数,记为 ⌊x⌋\lfloor x \rfloor⌊x⌋。让我们用它来检查我们的结果。

  • ⌊−3/2⌋=⌊−1.5⌋=−2\lfloor -3 / 2 \rfloor = \lfloor -1.5 \rfloor = -2⌊−3/2⌋=⌊−1.5⌋=−2。这与我们的移位结果相符。
  • ⌊−1/2⌋=⌊−0.5⌋=−1\lfloor -1 / 2 \rfloor = \lfloor -0.5 \rfloor = -1⌊−1/2⌋=⌊−0.5⌋=−1。这也与我们的移位结果相符。

这揭示了一个关于补码算术的美妙、普适的真理,一个你可以信赖的原则:对于任何有符号整数 xxx,​​算术右移 kkk 位在数学上等同于计算 ⌊x/2k⌋\lfloor x / 2^k \rfloor⌊x/2k⌋​​。 它总是执行向负无穷大取整的除法。对于正数,这与我们在小学学到的除法相同。对于负数,它具有这种精确、坚定不移的行为。

从数学真理到程序员的现实

这个普适真理使得算术移位如此强大,但它也带来了一个有趣的实际问题。大多数流行的编程语言,如 C、C++ 和 Java,规定整数除法必须​​截断​​,或向零取整。这意味着 7/27 / 27/2 是 333,而 −7/2-7 / 2−7/2 是 −3-3−3。

对于正数,没有问题。算术移位得到 ⌊7/2⌋=3\lfloor 7/2 \rfloor = 3⌊7/2⌋=3,这与截断相符。但对于负数,就存在冲突。算术移位得到 ⌊−7/2⌋=⌊−3.5⌋=−4\lfloor -7/2 \rfloor = \lfloor -3.5 \rfloor = -4⌊−7/2⌋=⌊−3.5⌋=−4,而语言要求的是 −3-3−3。

一个希望使用快如闪电的移位指令的编译器如何解决这个问题呢?它无法改变硬件。相反,它利用了一个源于对硬件行为深刻理解的巧妙技巧。对于负数 xxx,目标是计算 ⌈x/2k⌉\lceil x / 2^k \rceil⌈x/2k⌉。一个数学恒等式指出,这等于 ⌊(x+2k−1)/2k⌋\lfloor (x + 2^k - 1) / 2^k \rfloor⌊(x+2k−1)/2k⌋。由于算术移位 >> 已经计算了向下取整函数,编译器可以通过 (x + (1 k) - 1) >> k 来实现负数 x 除以 2k2^k2k 的截断除法。

应用与跨学科联系

我们花了一些时间拆解逻辑移位和算术移位精巧的内部机制,看到它们如何用不同的规则推拉着比特。这可能看起来像是一个相当形式化,甚至可能有些枯燥的练习。但现在,我们准备好迎接有趣的部分了。我们将像那些终于弄懂一套齿轮和杠杆如何工作的孩子们一样,开始建造奇妙的机器。在这段旅程中,我们将看到这些简单、基本的操作如何 blossoming(绽放)成我们计算世界的结构本身,塑造着从处理器硅心到软件创造性逻辑的一切,甚至在艺术和音乐的抽象世界中产生回响。

机器之心:打造处理器

如果你能把自己缩小到电子的大小,漫步在现代 CPU 的晶体峡谷中,你会发现其广阔如城市的景观中,有很大一部分是专门用于数据 перемешивание(洗牌)的。这种 перемешивание(洗牌)的核心是移位器,处理器自己的高速计算尺。

你会如何建造这样的东西?算术移位和逻辑移位之间的区别归结为一个单一、简单的选择:当我们向右移位时,用什么来填充空位?对于逻辑移位,我们总是用零填充。对于有符号数的算术移位,我们必须保留符号。正数以 000 开始,所以我们用 000 填充。负数以 111 开始,所以我们必须用 111 填充以保持其为负数。

硬件实现揭示了一种优雅的简洁性。在最高有效位的输入端,我们可以放置一个简单的 2-to-1 多路复用器——一个数字开关。开关的一个输入连接到常数 000。另一个输入连接到它即将替换的那个符号位本身。一个单一的控制信号,我们称之为 is_arithmetic,选择使用哪个输入。如果它是 false,我们得到 000(逻辑移位)。如果它是 true,我们得到旧的符号位(算术移位)。一个开关,一个决策,优雅地捕捉了整个逻辑上的区别。

如果出现 bug 怎么办?如果设计者在需要对负数进行除法时错误地使用了逻辑移位而不是算术移位,会怎样?其后果不是随机的混乱,而是一个可预测且往往是灾难性的错误。对于一个 n 位数,在负数上错误地执行 kkk 位的逻辑右移而不是算术右移,会导致结果精确地比正确答案大 2n−k2^{n-k}2n−k。这不仅仅是一个理论上的好奇心;这是硬件设计者必须严格测试的一种 bug,因为它会悄无声息地破坏任何执行有符号算术的程序。

当然,一次移一位对于现代处理器来说太慢了。为了获得高性能,我们使用*桶形移位器*,这是一种优美的组合逻辑电路,可以在一次迅速的操作中移动任意位数。桶形移位器通常是分层构建的。对于一个 32 位字,第一层可能移动 16 位或根本不移动。下一层移动 8 或 0 位,然后是 4、2,最后是 1 位。通过选择哪些层是活动的,我们可以组合出从 0 到 31 的任何移位量。这里的魔力在于,对于一个 n 位移位器,所需的层数不是 nnn,而只有 ⌈log⁡2(n)⌉\lceil \log_2(n) \rceil⌈log2​(n)⌉。这种对数级扩展是数字设计的胜利,将一个线性问题转变为一个对数问题。

在这里,我们再次看到了关注点分离的美妙。增加支持逻辑和算术移位的复杂性是否改变了这种巧妙的对数结构?完全没有。桶形移位器的深度是由移位的数量决定的。移位的类型——逻辑还是算术——仍然只是选择将哪个“填充位”送入移位器顶端的问题。核心结构保持不变,这是优雅工程的证明。

移位器构建好后,我们需要控制它。处理器的控制单元扮演着指挥家的角色,向管弦乐队的各个部分发送信号。在微程序处理器中,这些信号被编码在一个微指令中,这是一个宽控制字,其中不同的位域命令不同的动作。我们需要多少位来控制我们闪亮的新移位器?这是一个信息问题。如果我们需要指定逻辑移位、算术移位和循环移位,每种都有两个方向(左和右),我们就有了少数几个不同的操作。对于一个 32 位机器,移位量需要 5 位来编码从 0 到 31 的值(25=322^5 = 3225=32)。方向需要 1 位(左/右)。模式(逻辑、算术、循环)至少需要 2 位。总共,仅仅 8 位就可以完全指挥我们强大的移位器,这是信息如何被高效编码以控制复杂硬件的一个美丽例子。

再深入到单个逻辑门的层面,我们可以看到算术移位和逻辑移位之间的选择是如何具体化的。算术右移(SRA)的控制信号可能是布尔表达式 SRA=Shift∧Right∧SignSRA = \text{Shift} \land \text{Right} \land \text{Sign}SRA=Shift∧Right∧Sign,而逻辑右移(SRL)的控制信号是 SRL=Shift∧Right∧Sign‾SRL = \text{Shift} \land \text{Right} \land \overline{\text{Sign}}SRL=Shift∧Right∧Sign​。一个聪明的逻辑设计者会看到公共子表达式 Shift∧Right\text{Shift} \land \text{Right}Shift∧Right,并用一个共享的与门来实现它,将其输出馈送到另外两个门,这两个门再添加 Sign\text{Sign}Sign 和 Sign‾\overline{\text{Sign}}Sign​ 条件。这种逻辑共享,这种提取共性的做法,是其最根本层次的优化,节省了硅芯片上宝贵的面积。而整个链条,从有符号除法的高层概念到共享单个逻辑门,都必须经过严格测试。必须设计测试模式来探测细微的 bug,例如在移位超过一位时未能正确复制符号位,或错误处理大于字长的移位计数——这是确保机器计算出我们预期结果的关键步骤。

机器中的幽灵:编译器的艺术

硬件提供了原始工具,但软件——特别是编译器——才以狡猾的艺术性来运用它们。一个好的编译器是一位炼金术士,将我们人类可读的代码转化为高度优化的机器指令序列。移位是它最喜欢的工具之一。

其中一个经典的转换被称为强度削减:用一系列“更廉价”的移位和加/减法操作来替代像乘法这样的“昂贵”操作。想将一个数 xxx 乘以 7 吗?一个简单的处理器可能会在一个乘法指令上花费几个周期。但编译器知道 7=8−17 = 8 - 17=8−1。所以,它可以将 x×7x \times 7x×7 转换为 x×(8−1)x \times (8 - 1)x×(8−1),即 (x×8)−x(x \times 8) - x(x×8)−x。我们如何乘以 8 呢?那只是一个逻辑左移 3 位!昂贵的乘法被一个快如闪电的移位和一个减法所取代:(x 3) - x。

但是,正如科学中常有的情况,没有免费的午餐。这个巧妙的技巧带有一个隐藏的危险。MUL(乘法)指令可能只是计算一个值,但 SUB(减法)指令通常有一个副作用:它会更新处理器的状态标志(零、符号、进位、溢出)。想象一段代码,首先比较两个数 a 和 b,这会设置标志位。然后它执行我们强度削减后的计算 y = (x 3) - x。最后,它使用原始比较的标志位来进行条件跳转。我们“优化”序列中的 SUB 指令会覆盖或篡改这些标志,导致条件跳转基于垃圾信息做出决定。一个复杂的编译器必须意识到这一点,对状态标志进行“存活分析”,并在必要时采取纠正措施,例如在篡改指令后重新运行比较。这是优化与正确性之间的一场优美舞蹈。

编译器还利用了这些操作更深层、更抽象的属性。考虑表达式 (x >> 1) + (x >> 1) + x。乍一看,它似乎有点奇怪。但编译器将表达式表示为图,并且可以看到 x >> 1 是一个公共子表达式。它还可以看到 y + y 等同于 y 1。所以表达式简化为 ((x >> 1) 1) + x。现在到了精彩的部分。序列 (x >> 1) 1 实际上做了什么?它将一个位模式右移一位,然后再左移一位。最终效果是它将 x 的最低有效位置零。而且这一点是成立的,无论右移是逻辑的还是算术的。算术移位的符号扩展发生在高端;在低端移出的位无论如何都会丢失。这个普适真理允许编译器自信地将原始表达式重写为位运算形式 (x ~1) + x,这通常更高效。这证明了揭示位操作这些基本、不变的属性如何能够实现强大而可靠的优化。

超越处理器:在更广阔世界的回响

我们所看到的这些模式——这些精确的位操作——并不仅限于处理器设计和编译器的世界。它们是更深层数学结构的反映,这些结构出现在一些相当令人惊讶的地方。

现代计算中最强大的概念之一是并行性——同时做很多事情。专用硬件使用 SIMD(单指令,多数据)来将一个操作同时应用于整个数字向量。但即使在最基本的处理器上,我们也可以使用巧妙的位操作技巧来实现类似的并行性。想象一下,我们有两个由小的 8 位有符号数组成的向量,我们想计算它们的点积——这是人工智能、计算机图形学和数字信号处理的核心操作。我们可以将四个这样的 8 位数“打包”进一个 32 位字中。然后,使用移位和掩码,我们可以逐个提取每个 8 位块。在这里,逻辑移位和算术移位之间的区别至关重要。当我们提取像 10101010 这样的 8 位模式时,我们需要告诉处理器不要把它当作正数 170,而是当作负数 -86。为此,我们必须将其*符号扩展*到 32 位,用 1 填充所有新的高位。这正是算术右移的逻辑。通过应用这个逻辑,我们可以将相应的 8 位对相乘并将结果累加到一个累加器中,有效地以一个操作的代价执行了四个操作,所有这一切都由基本的位运算指令来编排。

也许最令人愉快和惊讶的应用将我们带出计算领域,进入音乐世界。在西方的十二音体系中,我们可以将所有音高的集合表示为从 0 到 11 的整数(C=0, C#=1, ..., B=11)。一个和弦,仅仅是一组音符,可以用一个 12 位的掩码来表示。例如,一个 C 大三和弦由音符 C、E 和 G 组成,它们对应于音高类别 0、4 和 7。我们可以用一个位掩码来表示这个和弦,其中第 0、4 和 7位置为 1:整数 20+24+27=1452^0 + 2^4 + 2^7 = 14520+24+27=145。

现在,什么是移调?它是将一个和弦在键盘上向上或向下移动。将 C 大三和弦上移一个半音会得到 C# 大三和弦。在音乐上,这是一种转换。在计算上,这是 12 位掩码的*循环移位*!将 C 大三和弦掩码的位左移一个位置,就将音符 C、E 和 G 移动到了 C#、F 和 G#,即 C# 大三和弦的音符。这揭示了一个基本的音乐操作和一个位运算之间惊人的同构关系。

有了这个模型,复杂的音乐问题就变成了简单的位运算。例如,一个给定和弦的哪些移调会完全落在一个给定的音阶内?(这是作曲和即兴创作中的一个常见问题)。我们也将音阶表示为一个掩码。如果一个和弦是音阶的子集,那么它就适合该音阶。在位掩码的世界里,这通过一个简单、优雅的检查来测试:(transposed_chord_mask scale_mask) == transposed_chord_mask。通过循环遍历 12 种可能的移调(循环移位)并应用此测试,我们可以立即找到我们和弦的所有“正确”位置。一个音乐理论问题就这样用计算机架构师的工具解决了。

从单个晶体管开关的微观决策,到桶形移位器的宏伟对数架构,再到编译器的微妙艺术,最后到音乐的抽象和谐,逻辑移位和算术移位之间的简单区别贯穿始终。它有力地提醒我们,在科学和工程中,最深远的应用往往源于最简单、最基本的思想。