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

5

当编译以下代码时:

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位除法?


代码生成在LLVM 11中更好,对于32位值有一个特殊情况。 - tambre
1个回答

6

考虑当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记录下它会做什么:

当将整数转换为带符号整数类型时,该值无法表示为该类型对象时产生的结果或引发的信号(C90 6.2.1.2、C99和C11 6.3.1.3)。

对于转换为宽度为N的类型,该值被模2^N减少,以使其在该类型的范围内;不引发信号。

所以需要生成代码以确保不会产生异常,并且返回的值是-2147483648(等同于2147483648 mod 2 ^ 32)。32位除法做不到这一点,但64位除法可以。

有趣的是,icc通过特殊处理i2 == -1只在该情况下执行64位除法,否则执行32位除法处理此问题。这可能是合理的,因为看一下Agner Fog的指令表,64位IDIV可能比32位IDIV昂贵几倍。尽管您可能希望它在这种情况下使用NEG而不是除法(如果您想知道,是的,INT_MIN的NEG是INT_MIN,就像期望的那样)。

对于乘法不需要进行这种特殊处理,因为imul在溢出时的行为已经符合转换的需要:将其截断为32位而不引发异常。


谢谢你找出了这个问题!实际上,我尝试了几种边界情况,但可能是我错过了这个或者测试有误。 - FPK
我认为,当你没有使用二进制补码算术时,除以-1而不是取反的反例会发生(尽管不要引用我)。 (虽然我怀疑这在实践中不是问题。) - CoffeeTableEspresso
@CoffeeTableEspresso:但我们正在讨论在使用二进制补码算术的机器上的实现。即使在C语言层面上,由于数学上x / -1 == -x成立,只要结果可以用适当类型表示,它就必须成立于整数算术中。 - Nate Eldredge
@NateEldredge 你可能是对的,但我有点想尝试弄清楚这是否真的是编译器编写者的疏忽,或者是否有一些我错过的边缘情况... - CoffeeTableEspresso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接