问题描述
让我们以 Add
和 Multiply
为例。哪一种可以归类为扩大?
假设输入是 2 的补码中的有符号字符(即长度为 8 位),除非另有声明。
正数加法
1+2 = 3
这个操作似乎没有扩大。 1、2 和 3 都适合一个字符。
然而,250 + 6
溢出了一个无符号字符。那么这是在扩大吗?
对于有符号类型也可以这样做,125 + 5
会将有符号字符溢出到符号位中。这是在扩大吗?
负数加法
-2-3 = -5
这会使二进制字符溢出 1 位:
1 1 1 1 1 1 1 0
+ 1 1 1 1 1 1 0 1
------------------
1 1 1 1 1 1 0 1 1
溢出通常会被丢弃,但是,这是否被视为扩大操作?
正数乘法
1 * 2 = 2
所有的乘法是否都在加宽,即使结果仍然适合原始数据类型?
上面的例子,2
仍然适合一个 8 位字符,但是,如果我用二进制手工计算,额外的 0s
会附加到结果的左侧,结果被丢弃。
负数乘法
-2 * -3 = 6
为简单起见,我们假设我们的输入是 4 位。这会按照预期的两倍大小溢出,即结果变为 8 位:
1110 * 1101 = 10110110
按预期丢弃高 4 位,低 4 位等于 6
。
这被认为是扩大操作吗?
分区
我没有详细介绍除法,但我认为它被认为是一种缩小操作。
解决方法
加宽与否也是执行数学运算的汇编指令的属性。但是,选择提供什么指令是由数学运算的固有属性决定的,例如乘法可以产生更广泛的结果。
在数学上,两个 n 位数字的和或差最多需要 n+1 位才能精确表示。例如无符号 3 位或二进制补码 4 位 7+7 = 14,即 0b1110
无符号或 0b01110
二进制补码。
大多数 CPU 仅提供 add
/sub
的非扩展实现,如果有的话,额外的位会进入进位标志。 (像 MIPS 这样的一些 ISA 根本没有 FLAGS,所以如果你想要结转,你必须从 carry = (a+b) < a
(无符号比较)中恢复它)。
在数学上,两个 n 位数字的乘积最多需要 2n 位才能精确表示。例如有符号的 8 位 -128 * -128 = 16384
SCHAR_MIN 平方(即 0x80 * 0x80 = 0x4000
,注意结果中的符号位为 0,但正下方的位被设置)。或无符号的 8 位 255 * 255 = 65025
,即 0xff ^ 2 = 0xfe01
。
许多 CPU 确实提供了一种扩展的形式乘法,例如x86 的 mul r/m32
和 imul r/m32
将产品写入 EDX:EAX。
x86 还具有非加宽 imul r,r/m32
,以防您不关心上半部分,例如为产生 unsigned * unsigned
结果的 unsigned
实现 C 语义,截断完整结果。 (请注意,无论将输入解释为有符号还是无符号,低半部分都是相同的,只有高半部分不同。这就是 x86 不费心提供类似 mul r,r/m32
指令的原因)
某些 ISA 不是提供一个加宽乘法,而是提供单独的低半和有符号高半/无符号高半指令。 (背靠背运行它们可能会让一些实现在内部将它们融合成一个单一的扩展乘法,但更简单的实现可以单独运行它们,而软件可能只使用其中一个。)
某些 ISA 甚至可能根本不提供获得高一半的方法,例如ARM Cortex-M0 微控制器只有 32b x 32b => 32b 乘法,没有 umull
。将其用作更宽乘法(如 64x64 => 128 位)的构建块可能意味着将输入分解为 16 位块,以应用通常的扩展精度技术,即 16b x 16b => 32b 乘法。>
在数学上,整数除法可以产生最多为被除数宽度的商。出于扩展精度的目的,支持更宽的除数很有用(请参阅 Why should EDX be 0 before using the DIV instruction?),但编译器通常不使用此功能。 (尤其不是在像 x86 这样的 ISA 上,如果商不适合 32 位,则 64b / 32b => 32b 除法会出错。)
但是是的,如果 ISA 有一个带有宽除数的除法指令,例如 x86 的隐式 EDX:EAX 用于 div
和 idiv
,这称为窄除法。
加宽加法/乘法意味着运算的结果大于至少一个输入。
特别是在 arm neon 中,加宽运算符不同于 (en)longening 指令,因此加宽加法/累加将 8 位源添加到 16 位目标,而长加法将两个 8 位源添加到 16 位目标。同样适用于 16 位和 32 位源。
0001+0002==00000003 加宽。
同样是 01*02==0002 加宽,02 不加。
在英特尔 x86 ISA 乘法中,乘法被设计为加宽,产生 32 位输出到寄存器对 dx:ax。同样,IA 的划分也在缩小。
相比之下,如果 int32/uint32 算术主要是模块化的,则 c/c++ 语义将丢弃无限精度结果的高位。