为了账号安全,请及时绑定邮箱和手机立即绑定

C+0x中的“while(1);”优化

C+0x中的“while(1);”优化

C++
SMILET 2019-07-10 14:46:44
C+0x中的“while(1);”优化我听说和阅读过C+0x允许编译器为下面的片段打印“Hello”#include <iostream>int main() {   while(1)      ;   std::cout << "Hello" << std::endl;}这显然与线程和优化功能有关。但在我看来,这会让很多人感到惊讶。有人能很好地解释为什么这是必要的吗?作为参考,最近的C+0x草案指出6.5/5一个循环,在for语句的for init语句之外,不调用库I/O函数,以及不访问或修改易失性对象,并且不执行同步操作(1.10)或原子操作(第29条)可由实现假定终止。[注意:这是为了允许编译器转换-mdash;,例如删除空循环,即使终止不能被证明。-尾注]编辑:这篇有见地的文章关于标准文本不幸的是,没有使用“未定义的行为”这个词。然而,每当标准说“编译器可以假定P”时,这就意味着具有非-P属性的程序具有未定义的语义。这是正确的,编译器是否允许为上述程序打印“Bye”?有一个更有洞察力的线在这里,这是一个与C类似的变化,由GuyDoes开始上面的链接文章。在其他有用的事实中,它们提供了一个解决方案,似乎也适用于C+0x(更新:这将不再适用于n 3225-见下面!)endless:   goto endless;编译器似乎不允许对其进行优化,因为它不是循环,而是跳转。另一个人总结了C+0x和C201x的拟议更改通过编写循环,程序员正在断言任一循环具有可见行为(执行I/O、访问易失性对象或执行同步或原子操作),或它最终会终止。如果我违反了这个假设,写了一个没有副作用的无限循环,我就是在欺骗编译器,而我的程序的行为是没有定义的。(如果我幸运的话,编译器可能会警告我。)语言不提供(不再提供?)一种在没有可见行为的情况下表示无限循环的方法。用n 3225更新3.1.2011:委员会将文本移至1.10/24并说该实现可能假定任何线程最终都将执行以下操作之一:终止,调用库I/O函数,访问或修改易失性对象,或执行同步操作或原子操作。这个goto特技威尔不继续工作!
查看完整描述

3 回答

?
Cats萌萌

TA贡献1805条经验 获得超9个赞

有人能很好地解释为什么这是必要的吗?

是的,Hans Boehm在N 1528:为什么无限循环的行为没有定义?虽然这是WG14文件,但理由也适用于C+,该文件同时提到WG14和WG21:

正如N 1509正确指出的那样,当前的草案本质上给出了6.8.5p6中无限循环的未定义行为。这样做的一个主要问题是,它允许代码在一个潜在的非终止循环中移动。例如,假设我们有以下循环,其中Count和Count 2是全局变量(或已获取它们的地址),而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指向循环列表而终止,那么原始循环就不会写到Count 2。因此,它可以与访问或更新Count 2的另一个线程并行运行。对于转换后的版本来说,这已经不再安全了,尽管存在无限循环,但它仍然可以访问Count 2。因此,转换可能会引入数据竞争。

在这种情况下,编译器很难证明循环终止;它必须理解Q指向一个非循环列表,我认为这超出了大多数主流编译器的能力,而且没有完整的程序信息通常是不可能的。

非终止循环所施加的限制是对编译器无法证明终止的终止循环的优化的限制,也是对实际非终止循环的优化的限制。前者比后者更常见,而优化往往更有趣。

显然还有带有整数循环变量的for-循环,编译器很难证明终止,因此编译器很难在没有6.8.5p6的情况下重构循环。甚至像

for (i = 1; i != 15; i += 2)

for (i = 1; i <= 10; i += j)

似乎很难处理。(在前一种情况下,需要一些基本的数论来证明终止,在后一种情况下,我们需要了解j的可能值。对无符号整数的环绕可能会进一步使这种推理复杂化。)

这个问题似乎适用于几乎所有的循环重构转换,包括编译器并行化和缓存优化转换,这两种转换都很可能变得重要,而且对数值代码通常也很重要。这似乎会变成一笔巨大的成本,以便能够以最自然的方式编写无限循环,特别是因为我们大多数人很少有意识地编写无限循环。

C的一个主要区别是c11为控制常量表达式提供了一个异常。它与C+不同,并使您的具体示例在C11中得到了很好的定义。


查看完整回答
反对 回复 2019-07-10
?
牛魔王的故事

TA贡献1830条经验 获得超3个赞

对我来说,有关的理由是:

这是为了允许编译器转换-mdash;,例如删除空循环,即使终止不能被证明。

想必,这是因为机械地证明终止是,如果不能证明终止,就会妨碍编译器进行有用的转换,例如将非依赖操作从循环前移到后面,反之亦然,在一个线程中执行循环后操作,而在另一个线程中执行循环,等等。如果没有这些转换,则在等待一个线程完成所述循环时,循环可能会阻塞所有其他线程。(我松散地使用“线程”来表示任何形式的并行处理,包括单独的VLIW指令流。)

编辑:愚蠢的例子:

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);}complex_io_operation();cout << "Results:" << endl;cout << x << endl;

在这里,一个线程执行complex_io_operation而另一个则在循环中做所有复杂的计算。但是,如果没有您引用的子句,编译器必须证明两件事才能进行优化:1)complex_io_operation()不依赖于循环的结果,和2)循环将终止..证明1)很容易,证明2)是停止的问题。使用该子句,它可能假定循环终止并获得并行化胜利。

我还设想设计人员认为,在生产代码中出现无限循环的情况非常罕见,通常是类似事件驱动的循环,以某种方式访问I/O。因此,他们悲观了罕见的情况(无限循环),而倾向于优化更常见的情况(非无限,但很难机械地证明为非无限循环)。

然而,这确实意味着,在学习示例时使用的无限循环将因此而受到影响,并将在初学者代码中引起棘手的问题。我不能说这完全是件好事。

编辑:关于您现在链接的有洞察力的文章,我会说“编译器可能假定程序的X”在逻辑上等同于“如果程序不满足X,行为是未定义的”。我们可以如下所示:假设存在一个不满足属性X的程序,该程序的行为将在哪里定义?标准只定义假设属性X为真的行为。尽管“标准”没有明确声明未定义的行为,但它通过省略声明未对其进行定义。

考虑一个类似的论点:“编译器可能假设变量x最多只在序列点之间分配一次”相当于“在序列点之间分配给x的次数超过一次”。


查看完整回答
反对 回复 2019-07-10
?
小怪兽爱吃肉

TA贡献1852条经验 获得超1个赞

我认为正确的解释来自您的编辑:空无限循环是未定义的行为。

我不认为这是特别直观的行为,但这种解释比另一种解释更有意义,即编译器被任意允许视而不见不调用UB的无限循环。

如果无限循环是ub,那就意味着非终止程序被认为没有意义:根据C+0x,它们没有语义学。

这也有一定的道理。它们是一个特例,许多副作用不再发生(例如,从没有从main),许多编译器优化由于必须保留无限循环而受到阻碍。例如,在循环中移动计算是完全有效的,如果循环没有副作用,因为最终,计算将在任何情况下执行。但是,如果循环从未终止,那么我们就不能安全地重新排列整个循环的代码,因为我们强权只需在程序挂起之前更改实际执行的操作。除非我们把绞刑程序当作UB,也就是说。


查看完整回答
反对 回复 2019-07-10
  • 3 回答
  • 0 关注
  • 507 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信