<long>/<long> 与 <int>/<int> 的区别

Difference of <long>/<long> vs. <int>/<int>

编译以下代码时:

int f(int i1,int i2)
{
    long l1=i1;
    long l2=i2;
    return l1*l2;
}

clang 10.1 得到 x86-64-O3,我得到

    mov     eax, edi
    imul    eax, esi
    ret

编译器识别出不需要完整的 64 位操作。

但是,当我用除法代替乘法时:

int f(int i1,int i2)
{
    long l1=i1;
    long l2=i2;
    return l1/l2;
}

编译成

    movsx   rax, edi
    movsx   rsi, esi
    cqo
    idiv    rsi
    ret

所以它使用了 64 位除法(gcc 也一样)。

这里阻止使用32位除法的反例是什么?

考虑当 i1 == INT_MIN == -2147483648i2 == -1 时会发生什么。

为了比较,我们也考虑一下

int g(int i1, int i2) {
    return i1/i2;
}

编译为简单的 32 位 idiv

如果您调用 g(INT_MIN, -1),除法将溢出,因为结果 2147483648 不适合 int。这会导致 C 级别的未定义行为,实际上 idiv 指令会产生异常。

如果您改为调用 f(INT_MIN, -1),除法不会溢出,因为结果确实适合 long。现在 return 语句通过通常的整数转换将其转换为 int。由于该值不适合签名类型int,结果是实现定义的,gcc documents它将做什么:

The result of, or the signal raised by, converting an integer to a signed integer type when the value cannot be represented in an object of that type (C90 6.2.1.2, C99 and C11 6.3.1.3).

For conversion to a type of width N, the value is reduced modulo 2^N to be within range of the type; no signal is raised.

所以需要生成代码保证不产生异常,并且返回的值为-2147483648(相当于2147483648 mod 2^32) . 32位除法不行,64位除法可以。

有趣的是,icc handles this 通过特殊外壳 i2 == -1 并且仅在这种情况下进行 64 位除法,否则进行 32 位除法。这可能是合理的,因为看起来 64 位 IDIV 可能比 32 位贵几倍,看一眼 Agner Fog 的指令表。尽管您可能希望在这种情况下它会使用 NEG 而不是除法(如果您想知道,是的,INT_MIN 的 NEG 是 INT_MIN 根据需要)。

(其实icc的-1的特殊大小写就是帮助我实现反例的提示。)

乘法不需要这样的特殊处理,因为 imul 溢出时的行为已经是转换所需要的:它毫无例外地截断为 32 位。