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

Why does GCC -O2 and -O3 optimization break this program?

我编写了这段 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) 但这只是作为随机测试完成的,以查看会发生什么,我将其发布在这里以找出到底发生了什么:)

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

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

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

  • 编译器或多或少直接翻译成机器代码,当溢出发生时它会做任何它想做的事情(通常是翻转到最负值)并且仍然包括测试(例如,如果值翻转将失败)
  • 编译器观察到索引变量从 3 开始并一直增加,因此循环条件始终成立,因此它发出一个从不费心测试循环条件的无限循环

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

if (i >= INT_MAX) break;

这将使您能够检查您的变量是否变得大于它可以容纳的整数。

循环是 for (i = 3; i > 2; i++),它没有 break 语句或其他退出条件。

最终 i 将达到 INT_MAX 然后 i++ 将导致整数溢出,从而导致未定义的行为。

可能 SumResult 也会先于 i 溢出。

当保证程序触发未定义行为时,程序的整个行为都是未定义的。

gcc 以积极优化触发 UB 的路径而闻名。您可以检查汇编代码以查看您的情况到底发生了什么。也许 -O2 和更高的情况删除了循环结束条件检查,但 -O1 将其保留在那里并且 "relied" 在 INT_MAX + 1 上导致 INT_MIN.

for 循环是 for(i=3; i>2; i++) 并且在这个循环中 i 没有被修改,也没有 break 或任何其他退出循环的方式。您依靠整数溢出来导致退出条件发生,但编译器没有考虑到这一点。

相反,编译器认为 i 从 3 开始,并且 i 只会递增,因此 i>2 始终为真。因此在这种情况下根本不需要 i 存在,因为这一定是一个无限循环。

如果把i改成unsigned int,设置循环退出的条件匹配,就不会出现这个"optimization"了。

我发现以下代码未经优化和 -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    [=11=]x10,%rsp
  400535:   c7 45 fc 03 00 00 00    movl   [=11=]x3,-0x4(%rbp)
  40053c:   c7 45 fc 03 00 00 00    movl   [=11=]x3,-0x4(%rbp)
  400543:   eb 04                   jmp    400549 <main+0x1c>
  400545:   83 45 fc 01             addl   [=11=]x1,-0x4(%rbp)
  400549:   83 7d fc 02             cmpl   [=11=]x2,-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    [=11=]x4005f4,%edi
  400559:   b8 00 00 00 00          mov    [=11=]x0,%eax
  40055e:   e8 ad fe ff ff          callq  400410 <printf@plt>
  400563:   b8 00 00 00 00          mov    [=11=]x0,%eax
  400568:   c9                      leaveq 
  400569:   c3                      retq  

输出为:-2147483648(正如我在 PC 上所期望的那样)

使用 -Os 代码结果:

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

我认为第二个结果是错误的!!!我认为编译器应该已经编译了对应代码的东西:

printf("%d\n",-2147483648);