为什么 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++
将导致整数溢出,从而导致未定义的行为。
可能 Sum
或 Result
也会先于 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);
我编写了这段 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++
将导致整数溢出,从而导致未定义的行为。
可能 Sum
或 Result
也会先于 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);