可以优化编译器删除无限循环,这不会改变任何数据,如
while(1)
/* noop */;
从分析编译器可以推导出的数据流图,这样的循环是“死代码”,没有任何副作用。
是否删除了C90 / C99标准禁止的无限循环?
C90或C99标准是否允许编译器删除此类循环?
更新:“Microsoft C版本6.0基本上完成了此优化。”,请参阅caf的链接。
label: goto label;
return 0;
将转换为
return 0;
答案 0 :(得分:25)
C11澄清了这个问题的答案,在草案C11标准部分6.8.5
迭代声明中添加了以下段落:
一个迭代语句,其控制表达式不是常量 表达式, 156)不执行任何输入/输出操作 访问volatile对象,不执行同步或原子 在其身体中的操作,控制表达,或(在一个案例中) 对于语句,其表达式-3可以由实现承担 终止。 157)
和脚注157
说:
这是为了允许编译器转换,例如即使在删除空循环时也是如此 终止无法证明。
所以你的具体例子是:
while(1)
/* noop */;
由于控制表达式是一个常量表达式,因此不是公平的优化游戏。
无限循环为UB
那么为什么编译器允许使用上面提供的异常优化掉无限循环,Hans Boehm提供了在N1528: Why undefined behavior for infinite loops?中进行无限循环未定义行为的基本原理,以下引用给出了对所涉及问题的良好感觉:
正如N1509正确指出的那样,目前的草案基本上给出了 6.8.5p6中无限循环的未定义行为。一个主要问题 这样做是因为它允许代码在潜在的范围内移动 非终止循环。例如,假设我们有以下循环, 其中count和count2是全局变量(或者有地址 并且p是一个局部变量,其地址尚未被采用:
for (p = q; p != 0; p = p -> next) { ++count; } for (p = q; p != 0; p = p -> next) { ++count2; }
这两个循环是否可以合并并由以下循环替换?
for (p = q; p != 0; p = p -> next) { ++count; ++count2; }
没有6.8.5p6中的无限循环的特殊分配,这个 将被禁止:如果第一个循环因q而终止 指向循环列表,原始永远不会写入count2。从而 它可以与访问或的另一个线程并行运行 更新count2。对于转换后的版本,这已不再安全 尽管存在无限循环,它仍会访问count2。就这样 转型可能引入数据竞争。
在这种情况下,编译器不太可能 证明循环终止;它必须要理解q点 一个非循环的列表,我相信这是超出大多数人的能力 主流编译器,没有整个程序通常是不可能的 信息。
C99
由于C99没有这样做,我们会查看5.1.2.3
部分中涵盖的 as-if规则,它基本上说编译器只需模拟可观察的行为一个程序,要求如下:
符合实施的最低要求是:
- 在序列点处,易失性对象在先前访问的意义上是稳定的 尚未发生完整和后续访问。
- 在程序终止时,写入文件的所有数据应与结果相同 根据抽象语义执行程序会产生。
- 交互设备的输入和输出动态应按照规定进行 7.19.3。这些要求的目的是无缓冲或行缓冲输出 尽快出现,以确保提示消息实际出现在之前 一个等待输入的程序。
严格阅读这一点似乎允许实现优化无限循环。我们当然可以提出这样的场景,即优化掉无限循环会导致可观察行为发生变化:
while(1) ;
printf( "hello world\n" ) ;
许多人认为影响一个过程的终止也是可观察到的行为,这个位置是在C Compilers Disprove Fermat’s Last Theorem中采用的:
编译器在如何实现C程序方面具有相当大的自由度,但其输出必须具有与标准中描述的“C抽象机器”解释时程序将具有的相同的外部可见行为。许多知识渊博的人(包括我)都认为这不能改变程序的终止行为。显然有些编译器不同意,或者不相信它很重要。合理的人不同意这种解释这一事实似乎表明C标准存在缺陷。
更新
我在某种程度上错过了上述文章Compilers and Termination Revisited的后续行动,该文章对5.1.2.3
部分进行了以下说明:
第二个要求是棘手的。如果它正在谈论在抽象机器上运行的程序的终止,那么它是空洞的,因为我们的程序没有终止。如果它正在讨论由编译器生成的实际程序的终止,则C实现是错误的,因为写入文件(stdout是文件)的数据与抽象机器写入的数据不同。 (这个读数归功于Hans Boehm;我未能将这种微妙之处取出标准。)
人们还可以提出一个较弱的论点,即需要在C11中创建一个允许空循环删除的需要,这意味着以前这不是一个允许的优化。
这是否也适用于无限goto循环?
我相信这也意味着这也适用于无限的goto循环。在C ++中,显然就是这种情况,因为1.10
[intro.multithread] 部分说:
实现可以假设任何线程最终将执行以下某个
- 终止,
- 调用库I / O函数,
- 访问或修改易失性对象,或
- 执行同步操作或原子操作。
然后在N1528
中表达的意图是C和C ++标准一致:
由于编译器后端通常在C和C ++编译器之间共享,因此最重要的是WG14和WG21对采用的解决方案达成一致。替代方案是通过后端对两种语言进行特殊处理,或者在处理C代码时禁用优化。两者都不合适。
最后说:
WG21正在考虑改进措辞,使治疗保持一致。希望WG14能够追踪任何由此产生的变化。
目前C11标准不包含5.1.2.4
多线程执行和数据竞赛部分中的类似措辞,但考虑到N1528
,假设编译器将处理无限的似乎是明智的goto循环为C和C ++中未定义的行为。
另请注意,请参阅US comment 38 here和N3196,这是应用此更改的论文。
答案 1 :(得分:9)
无法普遍检测无限循环:请参阅the Halting Problem。所以任何编译器都可以做的最好的事情就是采取一个不错的猜测 - 例如OP中提到的明显情况。
但为什么这是可取的呢?我可以看到发出警告并仍然允许行为,但删除循环不是“优化” - 它改变了程序的行为!
答案 2 :(得分:8)
循环不是死代码,它基本上阻止程序达到它后面的任何东西。如果删除循环,则不会发生这种情况,因此编译器无法删除循环。
它可能会用平台相关的空闲指令替换它,以通知处理器该线程不再执行任何操作。
编译器可以做的是删除循环后出现的任何代码,因为它无法访问,永远不会被执行。
答案 3 :(得分:4)
在comp.lang.c
(例如here)之前已经多次讨论过这个问题,据我所知,没有任何共识结果。
答案 4 :(得分:3)
在编写守护进程时,它们是必需的。你为什么要把它们称为死代码?
答案 5 :(得分:0)
我认为较新的标准明确规定,如果一段代码不访问任何易变变量,执行I / O等,任何其他不依赖于第一块中计算的任何代码的代码可以在它之前被任意排序。 。如果无限循环不执行任何I / O,也不计算稍后在程序中使用的任何值,则编译器可以简单地推迟循环的执行,直到其他所有内容都完成。