【问题标题】:signed and unsigned arithmetic implementation on x86x86 上的有符号和无符号算术实现
【发布时间】:2014-10-04 04:12:21
【问题描述】:

C 语言有有符号和无符号类型,如 char 和 int。 我不确定它是如何在装配级别实现的,因为 例如,在我看来,有符号和无符号的乘法 会带来不同的结果,所以组装都没有签名 和有符号算术或只有一个,这在某种程度上是 模拟不同的情况?

【问题讨论】:

标签: c algorithm math x86 integer-arithmetic


【解决方案1】:

如果你查看 x86 的各种乘法指令,只查看 32 位变体而忽略 BMI2,你会发现这些:

  • imul r/m32 (32x32->64 有符号乘法)
  • imul r32, r/m32 (32x32->32 乘) *
  • imul r32, r/m32, imm (32x32->32 乘) *
  • mul r/m32 (32x32->64 无符号乘法)

请注意,只有“加宽”乘法具有无符号对应项。中间用星号标记的两种形式都是有符号和无符号乘法,因为对于没有得到额外“上部”的情况,这是一回事。 p>

“加宽”乘法在 C 中没有直接等价物,但编译器可以(并且经常)使用这些形式。

例如,如果你编译这个:

uint32_t test(uint32_t a, uint32_t b)
{
    return a * b;
}

int32_t test(int32_t a, int32_t b)
{
    return a * b;
}

使用 GCC 或其他一些相对合理的编译器,你会得到这样的结果:

test(unsigned int, unsigned int):
    mov eax, edi
    imul    eax, esi
    ret
test(int, int):
    mov eax, edi
    imul    eax, esi
    ret

(使用 -O1 的实际 GCC 输出)


因此,符号对于乘法(至少对于您在 C 中使用的那种乘法)和其他一些操作而言并不重要,即:

  • 加法和减法
  • 按位与、或、异或、非
  • 否定
  • 左移
  • 比较平等

x86 不提供单独的签名/未签名版本,因为无论如何都没有区别。

但是对于某些操作是有区别的,例如:

  • 师(idiv vs div
  • 余数(还有idivdiv
  • 右移(sar vs shr)(但要注意 C 中的有符号右移)
  • 比较大于/小于

但最后一个很特殊,x86 也没有单独的签名和未签名版本,而是有一个操作(cmp,实际上只是一个非破坏性的sub),可以同时执行这两个操作,并给出几个结果(“标志”中的多个位受到影响)。稍后实际使用这些标志(分支、条件移动、setcc)的指令然后选择他们关心的标志。比如,

cmp a, b
jg somewhere

如果a 的“签名大于”b,则将转到somewhere

cmp a, b
jb somewhere

如果a 是“下方未签名”b,则会转到somewhere

有关标志和分支的更多信息,请参阅Assembly - JG/JNLE/JL/JNGE after CMP


这不会是有符号和无符号乘法相同的正式证明,我只是尝试让您了解为什么它们应该相同。

考虑 4 位 2 的补码整数。它们各个位的权重是从 lsb 到 msb、1、2、4 和 -8。当您将其中两个数字相乘时,您可以将其中一个数字分解为与其位相对应的 4 个部分,例如:

0011 (decompose this one to keep it interesting)
0010
---- *
0010 (from the bit with weight 1)
0100 (from the bit with weight 2, so shifted left 1)
---- +
0110

2 * 3 = 6 所以一切都检查出来了。这只是大多数人在学校学习的常规长乘法,只有二进制,这使它更容易,因为你不必乘以十进制数字,你只需乘以 0 或 1,然后移位。

不管怎样,现在取一个负数。符号位的权重为 -8,因此在某一时刻,您将生成部分乘积 -8 * something。乘以 8 是左移 3,所以前一个 lsb 现在是 msb,所有其他位都是 0。现在如果你否定它(它毕竟是 -8,而不是 8),什么也不会发生。零显然没有变化,但 8 也是如此,而且通常只有 msb 集的数字:

-1000 = ~1000 + 1 = 0111 + 1 = 1000

因此,如果 msb 的权重为 8(如无符号情况)而不是 -8,那么您已经做了同样的事情。

【讨论】:

  • 我不确定我是否理解 - 这是否意味着因为 C 不支持 32*32->64 (或者他是否像“unsigned64 c = a*b;”那样工作?没有区别吗?它仍然没有回答所有问题,或者我不明白答案 - 如果在汇编级别上仅支持有符号算术或仅支持无符号算术,完全/部分,或两者兼而有之?9对不起,这个问题有点复杂)
  • @user2214913 你可以写uint64_t res = a * b 但这会导致 narrow 结果,但随后会变宽。或者你可以写uint64_t res = a * (uint64_t)b 并得到宽的结果,但是你真的有一个 64x64->64 乘法,其中操作数的高位恰好为零(但这是编译器 可能的事情之一 在某些情况下使用 32x32->64 mul 实现)。虽然不是真的一样。至于支持(未)签名算术的程序集,这个答案(和另一个答案)的重点是表明它们无论如何都是一样的。
  • @user2214913 非常确定。另外,我将说明为什么有符号和无符号乘法是相同的
  • @user2214913 在uint64_tdouble 之间的转换不受支持,但签名版本支持。我认为这就是全部,就我现在所能想到的而言,所有东西要么都有签名版本和未签名版本,要么不需要两者。 (此处忽略向量指令)
  • stackoverflow.com/a/4040834/995714 "imul 更强大,因为它接受使用有点任意的操作数寄存器,而 mul 必须使用 eax 作为输入之一,并将结果写入 edx:eax。imul 使它更容易对于编译器...由于 C 编译器使用 imul 而不是 mul,因此 Intel 和 AMD 在优化 imul 方面投入了比 mul 更多的努力,使得前者在最近的处理器中更快。这使得 imul 更具吸引力。”
【解决方案2】:

大多数现代处理器都支持有符号和无符号算术。 对于不支持的算术,我们需要模拟算术。

引用 this answer 的 X86 架构

首先,x86 原生支持二进制补码 有符号数的表示。您可以使用其他表示 但这需要更多的说明,通常是浪费 处理器时间。

我所说的“原生支持”是什么意思?基本上我的意思是有一个 一组用于无符号数的指令和另一组 您用于签名号码。无符号数可以放在同一个 注册为带符号的数字,实际上您可以混合带符号和 无符号指令而不用担心处理器。这取决于 编译器(或汇编程序员)来跟踪一个数字是否是 签名与否,并使用适当的说明。

首先,二进制补码具有加法和 减法与无符号数相同。它没有 数字是正数还是负数的区别。 (所以你只要 继续添加和订阅您的号码,不用担心。)

在比较时,差异开始显现。 x86 有一个 区分它们的简单方法:上方/下方表示无符号 比较和大于/小于表示有符号比较。 (例如。 JAE 的意思是“如果高于或等于就跳转”并且是无符号的。)

还有两组乘法和除法指令 处理有符号和无符号整数。

最后:如果你想检查溢出,你会这样做 有符号数和无符号数不同。

【讨论】:

  • tnx 回答虽然我必须说我还是不明白
  • 主要是2点,C规范只是指定了有符号和无符号类型的行为。他们没有指定如何实现这种行为。如果处理器支持所需的操作,则使用这些操作来实现,否则编译器编写者选择使用可用的有限指令集来实现该行为。这就是为什么大多数编译器编写者选择 2s 补码作为负数的原因,因为 2s 补码的加法/减法与无符号数相同。在 X86 上,有符号/无符号乘法有不同的指令。
  • 有符号和无符号似乎不一样,例如 -200 * -200 (说到字节)将带来与 unsigned(-200) * unsigned(-200) 相同?我有点迷失在这里。所以你说c不支持窄参数的扩大乘法?你确定吗?
  • 是的,由于以下选择,产品是相同的。 -a 在无符号中变为 (m-a),其中 m 是 2^(CHAR_BITS * sizeof(unsigned int))。无符号 (-a) * 无符号 (-a) = (ma) * (ma) = m(m-2a) + aa = aa (C 保证环绕无符号整数类型)= -a * -a
【解决方案3】:

cmpsub 的一点补充。我们知道cmp 被认为是无损sub,所以让我们关注sub

当 x86 cpu 执行sub 指令时,例如,

sub eax, ebx

cpu 如何知道 eax 或 ebx 的值是有符号还是无符号?例如,考虑二进制补码中的 4 位宽数:

eax: 0b0001
ebx: 0b1111

在有符号或无符号中,eax 的值将被解释为1(dec),这很好。

但是,如果ebx是无符号的,则会被解释为15(dec),结果变成:

ebx:15(dec) - eax: 1(dec) = 14(dec) = 0b1110 (two's complement)

如果 ebx 已签名,则结果变为:

ebx: -1(dec) - eax: 1(dec) = -2(dec) = 0b1110 (two's complement)

即使对于有符号或无符号,它们的结果在二进制补码中的编码都是相同的:0b1110

但是一个是正数:14(dec),另一个是负数:-2(dec),然后回到我们的问题:cpu如何分辨哪个到哪个?

答案是 cpu 将同时评估两者,来自:http://x86.renejeschke.de/html/file_module_x86_id_308.html

它评估有符号和无符号整数操作数的结果,并设置 OF 和 CF 标志以分别指示有符号或无符号结果中的溢出。 SF标志表示签名结果的符号。

对于这个具体的例子,当cpu看到结果:0b1110时,它会将SF标志设置为1,因为如果0b1110被解释为负数,它就是-2(dec)

那么这取决于他们是否需要使用 SF 标志或简单地忽略它。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-06-07
  • 2011-06-23
  • 2021-07-30
  • 2013-10-28
  • 2012-12-13
  • 2015-02-24
  • 2014-02-23
相关资源
最近更新 更多