【问题标题】:Difference of <long>/<long> vs. <int>/<int><long>/<long> 与 <int>/<int> 的区别
【发布时间】:2020-09-03 11:24:48
【问题描述】:

编译以下代码时:

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

【问题讨论】:

标签: c optimization compiler-construction clang compiler-optimization


【解决方案1】:

考虑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,结果是implementation-defined,gcc documents 会做什么:

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

为了转换为宽度为 N 的类型,该值以 2^N 为模减少到该类型的范围内;没有发出信号。

所以它需要生成确保不产生异常的代码,并且返回的值是-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 位。

【讨论】:

  • 感谢您的了解!实际上,我尝试了几个边界案例,要么我错过了这个,要么测试错了。
  • 我相信当您不使用二进制补码算术时,会发生除以 -1 而不是否定的反例(尽管不要引用我的话)。 (虽然我怀疑这在实践中是个问题。)
  • @CoffeeTableEspresso:但我们说的是在确实使用二进制补码算法的机器上实现。即使在 C 级别,由于 x / -1 == -x 在数学上是正确的,所以只要结果可以用适当的类型表示,它就必须适用于整数运算。
  • @NateEldredge 你可能是对的,但我有点想弄清楚这是否真的是编译器编写者的疏忽,或者我错过了一些边缘情况......
猜你喜欢
  • 1970-01-01
  • 2011-05-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-19
  • 2017-09-10
  • 2010-12-27
  • 1970-01-01
相关资源
最近更新 更多