为什么使用GCC的-O2和-O3优化会破坏这个程序?

4

我写了这段C代码来找到所有的整数和,这些整数等于其数字的阶乘之和。如果没有GCC优化标志,在一分钟左右就可以完成工作,使用-O1将时间缩短了约15-20秒,但当我尝试使用-O2、-O3或-Os时,它会陷入无限循环。

int main()
{
  int i, j, factorials[10];
  int Result=0;

  for(i=0; i<10; i++)
  {
    factorials[i]=1;
    for(j=i; j>0; j--)
    {
      factorials[i] *= j;
    }
  }

  for(i=3; i>2; i++) //This is the loop the program gets stuck on
  {
    int Sum=0, number=i;

    while(number)
    {
      Sum += factorials[number % 10];
      number /= 10;
    }

    if(Sum == i)
      Result += Sum;
  }

  printf("%d\n", Result);  
  return 0;
}

我已经确定for(i=3; i>2; i++)是问题的根源。那么显然,i永远不会小于2?

这是否与整数溢出行为未定义有关?如果是,那么在这些情况下程序到底发生了什么?

编辑:我想我应该提到,我知道编写这个for循环的其他方式,以避免使用溢出操作(我希望INT_MAX+1等于INT_MIN,即<2),但这只是一个随机测试,我在这里发布它是为了找出到底发生了什么 :)


一旦您调用未定义的行为,所有的赌注都是无效的。使用-funsafe-loop-optimizations尝试godbolt示例可以很好地说明这一点,我们可以看到gcc将其转换为一个什么也不做的无限循环。还可以参见这个这个 - Shafik Yaghmour
3
不是gcc导致你的代码出错,而是你的代码本身有问题,在使用gcc进行优化时就会显现出来。 - too honest for this site
@ShafikYaghmour 如果完全删除无限循环(没有常量控制表达式或可观察行为的无限循环会导致UB),也是相当合理的,我有点惊讶它保留了循环。 - M.M
@M.M 的确,由于这是优化效果,选择这种方式可能有更深层次的原因。不过我不确定。 - Shafik Yaghmour
i 达到最大正值时,它会变成小于 2,然后(使用二进制补码表示)它就成为了一个负数所能表示的最大绝对值! - Sir Jo Black
显示剩余3条评论
6个回答

7
循环是for (i = 3; i > 2; i++),没有break语句或其他退出条件。最终i将达到INT_MAX,然后i++会导致整数溢出,从而导致未定义的行为。可能在i之前SumResult也会溢出。当程序保证触发未定义的行为时,整个程序的行为都是未定义的。gcc以激进的优化出名,可以检查汇编代码以了解在您的情况下发生了什么。也许-O2和更高版本会删除循环结束条件检查,但-O1会留下它,并“依赖”于INT_MAX + 1 导致INT_MIN

如果您使用启用了优化的gcc修改我的godbolt示例,它会将其转换为无条件jmp,而在没有优化的情况下,它会检查变量。因此,您确实是正确的。 - Shafik Yaghmour

1
for循环是for(i=3; i>2; i++),在这个循环内部,i没有被修改,也没有break或其他任何退出循环的方式。你依靠整数溢出来引发退出条件,但编译器并不考虑这一点。
相反,编译器看到 i 从3开始,并且 i 只会被增加,所以 i>2 总是成立。因此,在这种情况下,i 实际上并不需要存在,因为这必须是一个无限循环。
如果你将i更改为unsigned int并设置循环退出条件来匹配,那么这个“优化”就不再发生了。

1

我发现以下代码的汇编结果在没有优化和使用-Os优化编译时有很大差异,这让我感到非常奇怪。

#include <stdio.h>

int main(){
    int i;

    for(i=3;i>2;i++);

    printf("%d\n",i);

    return 0;
}

没有优化的代码结果:

000000000040052d <main>:
  40052d:   55                      push   %rbp
  40052e:   48 89 e5                mov    %rsp,%rbp
  400531:   48 83 ec 10             sub    $0x10,%rsp
  400535:   c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%rbp)
  40053c:   c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%rbp)
  400543:   eb 04                   jmp    400549 <main+0x1c>
  400545:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  400549:   83 7d fc 02             cmpl   $0x2,-0x4(%rbp)
  40054d:   7f f6                   jg     400545 <main+0x18>
  40054f:   8b 45 fc                mov    -0x4(%rbp),%eax
  400552:   89 c6                   mov    %eax,%esi
  400554:   bf f4 05 40 00          mov    $0x4005f4,%edi
  400559:   b8 00 00 00 00          mov    $0x0,%eax
  40055e:   e8 ad fe ff ff          callq  400410 <printf@plt>
  400563:   b8 00 00 00 00          mov    $0x0,%eax
  400568:   c9                      leaveq 
  400569:   c3                      retq  

输出为:-2147483648(在PC上我预期的结果)

使用 -Os 编译代码结果:

0000000000400400 <main>:
  400400:   eb fe                   jmp    400400 <main>

我认为第二个结果是错误的!!! 我认为编译器应该编译与以下代码相对应的内容: printf("%d\n",-2147483648);

1
编译器不需要编译与 printf() 相应的代码,因为一旦 int 溢出,它就没有定义的行为,所以 i<2 永远不会发生,而 for 循环在功能上等同于 while(true)。由于循环内没有 break 语句,printf() 函数没有通向它的代码路径,因此可以进行优化处理。 - sig_seg_v

0

正如您自己注意到的那样,有符号整数溢出是未定义的。编译器决定推理您的程序,假设您足够聪明,永远不会导致未定义的行为。因此,它可以得出结论,由于i初始化为大于2的数字并且只会递增,它永远不会小于或等于2,这意味着i > 2永远不可能为false。这反过来又意味着循环永远不会终止,并且可以优化为无限循环。


0

正如你所说,这是未定义的行为,因此您不能依赖于任何特定的行为。

您最有可能看到的两件事是:

  • 编译器将更多或更少地直接转换为机器代码,当溢出发生时(通常是滚动到最负值),它会执行任何它想要执行的操作,并仍然包括测试(例如,如果值滚动,则失败)
  • 编译器观察到索引变量从3开始并且始终增加,因此循环条件始终成立,因此它发出一个永无止境的循环,从不费心测试循环条件

0

我不知道你在尝试什么,但如果你想处理整数溢出,只需在源代码中包含limits.h并在for循环内写下这行代码。

if (i >= INT_MAX) break;

这将使您能够检查变量是否超出整数范围。


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