程序在在线IDE上表现异常


82

我遇到了以下C ++程序(源代码):

#include <iostream>
int main()
{
    for (int i = 0; i < 300; i++)
        std::cout << i << " " << i * 12345678 << std::endl;
}

它看起来像一个简单的程序,并在我的本地计算机上提供正确的输出,例如:

0 0
1 12345678
2 24691356
...
297 -628300930
298 -615955252
299 -603609574

但是,在诸如codechef之类的在线IDE上,它提供以下输出:

0 0
1 12345678
2 24691356
...
4167 -95167326
4168 -82821648
4169 -7047597

为什么for循环不终止于300?此程序也总是在终止4169。为什么4169而不是其他一些价值?


45
可能是因为有符号整数溢出具有不确定的行为。
eerorika

17
我知道签名溢出是UB,但这是一个失败的失败……
Galik

45
一个很好的教训,不要假设UB受限制
MM

12
我很好奇您为什么认为第一个输出是“正确的输出”。
Lightness Races in Orbit

5
@LightnessRacesinOrbit -嗯,这提醒有价值的!
davidbak

Answers:


108

我将假设在线编译器使用GCC或兼容的编译器。当然,任何其他编译器也可以进行相同的优化,但是GCC文档很好地说明了它的作用:

-faggressive-loop-optimizations

此选项告诉循环优化器使用语言约束来得出循环迭代次数的界限。假定循环代码不会通过例如导致有符号整数溢出或超出范围的数组访问来调用未定义的行为。循环迭代次数的界限用于指导循环展开和剥离以及循环退出测试优化。默认情况下启用此选项。

此选项仅允许基于证明UB的情况进行假设。为了利用这些假设,可能需要启用其他优化,例如恒定折叠。


有符号整数溢出具有不确定的行为。优化器能够证明任何i大于173的值都将导致UB,并且因为它可以假定不存在UB,所以它也可以假设该i值永远不会大于173。然后可以进一步证明i < 300始终为真,并且因此可以优化循环条件。

为什么是4169,而不是其他值?

这些站点可能会限制它们显示的输出行数(或字符或字节),并且碰巧共享相同的限制。


3
如果编译器可以证明存在i大于173的UB ,那么为什么不发出警告而不是毫无意义的优化呢?
Jabberwocky

7
@MichaelWalz它确实发出一个。
HolyBlackCat

3
@MichaelWalz:删除冗余的布尔测试不是毫无意义的优化;这是一个非常有用的。什么是(主要)毫无意义是增加额外的代码来禁用/撤消断源代码中存在的优化。

25
@MichaelWalz:编译器无法按照您的建议可靠地检测到UB(尽管有时它会警告可能的事件,实际上在这里这样)。相反,它可以在没有UB的假设下以最佳意图进行。那些也许是两个微妙但实际上却截然不同的东西。这不像编译器去了“ aha!UB!现在我可以启用这样的优化” —优化始终存在。它在做这样的东西所有的时间。但是,只要正确编写了程序,就不会见证其潜在语义的任何变化。
Lightness Races in Orbit

20
打个比方,房子前门的制造商可能已经决定,如果在战术上将​​一块金属放在中间的某处,它会更坚固。您永远不会注意到,除非您在门上打孔并因此错误地使用门
Lightness Races in Orbit

40

“未定义的行为是未定义的。” (C)

在codechef上使用的编译器似乎使用以下逻辑:

  1. 未定义的行为不会发生。
  2. i * 12345678溢出并导致UB if i > 173(假设32位ints)。
  3. 因此,i永远不能超过173
  4. 因此i < 300是多余的,可以替换为true

循环本身似乎是无限的。显然,codechef只是在特定时间后停止程序或截断输出。


11
@ArpanMangal我只是计算了输出中的字符,好像是2^16。显然,这都是将输出截断为2^16字符的巧合。
HolyBlackCat

3
@ArpanMangal:像4169这样的鼻恶魔,目前正在ideone和codechef聚会。UB是不确定的,可以是任何东西,包括鼻恶魔。认真地说,尝试分析UB是浪费时间,请利用这段时间弄清楚如何阻止它发生。
jmoreno,

6
@jmoreno如果您对编译器设计感兴趣,这不是浪费时间
user253751 '18

2
@jmoreno虽然,这一次可以分析它。了解UB可以精确分解的内容可能有助于得出在什么情况下UB可以接受(如果有)的结论。
HolyBlackCat

4
@jmoreno宇宙所做的一切都是正确的(根据定义),那么研究天文学的意义何在?
user253751 '02

11

您正在调用未定义行为的内部的第174次迭代可能for环路最高int值可能是2147483647尚未174 * 123456789表达式求2148147972是因为有整数溢出没有签署未定义行为。因此,您正在观察UB的影响,尤其是在您的案例中设置了优化标志的GCC编译器中。编译器可能会通过发出以下警告来就此警告您:

warning: iteration 174 invokes undefined behavior [-Waggressive-loop-optimizations]

删除(-O2)优化标记以观察不同的结果。


4
值得注意的是,未定义的行为可能具有追溯力-因为UB会在第174次迭代中发生,所以该标准甚至不要求循环的前173次迭代应按预期进行!

@Hurkyl确实。UB导致整个程序(包括前面的语句都显示UB)有点矛盾。
罗恩

12
@罗恩:这不是矛盾的。该程序要么具有定义明确的语义,要么没有。期。请记住,C ++代码不是要执行的指令序列;它是程序的描述
Lightness Races in Orbit

@LightnessRacesinOrbit:程序可能具有部分未指定但并非完全未定义的语义。尽管C标准使用的语言有点含糊,但它试图将其应用到所谓的“绑定的UB”中。允许编译器在处理整数溢出之类的问题时具有广泛但并非无限的自由,不会干扰许多其他有用的优化,但会使该语言更适合处理从不可靠来源收到的数据。
超级猫

@supercat:确实,未指定的行为与未定义的行为不同。我们正在讨论后者
亮度种族在轨道

7

编译器可以假定不会发生未定义的行为,并且由于带符号的溢出是UB,因此可以假定它从不i * 12345678 > INT_MAX,因此也i <= INT_MAX / 12345678 < 300可以取消检查i < 300

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.