优化掉“while(1);” 在C ++ 0x

更新,见下文!

我听说过,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章)
  • 可能由实施假定终止。 [注意:这是为了允许编译器转换,例如删除空循环,即使无法证实终止。 - 结束注释]

    编辑:

    这篇富有洞察力的文章谈到了这个标准文本

    不幸的是,没有使用“未定义的行为”这个词。 但是,只要标准说“编译器可能会承担P”,就意味着具有属性not-P的程序具有未定义的语义。

    这是否正确,编译器是否允许为上述程序打印“再见”?


    这里有一个更有洞察力的线索,这是对C的一个类似的变化,由Guy做了上面的链接文章开始。 除了其他有用的事实,他们提出了一个解决方案,似乎也适用于C ++ 0x(更新:这将不再适用于n3225 - 见下文!)

    endless:
      goto endless;
    

    编译器似乎不允许优化它,因为它不是循环,而是跳转。 另一个人总结了C ++ 0x和C201X中提出的改变

    通过编写一个循环,程序员断言循环执行某些具有可见行为的事情(执行I / O,访问易失性对象,或者执行同步或原子操作),或者它最终终止。 如果我通过编写一个没有副作用的无限循环来违反这个假设,我对编译器说谎,并且我的程序的行为是不确定的。 (如果我很幸运,编译器可能会提醒我。)该语言没有提供(不再提供?)一种表达无限循环而没有可见行为的方式。


    更新3.1.2011与n3225:委员会提出的文字为1.10 / 24并说

    实现可能会假定任何线程最终都会执行以下操作之一:

  • 终止,
  • 拨打图书馆的I / O功能,
  • 访问或修改易失性对象,或
  • 执行同步操作或原子操作。
  • goto技巧将无法工作了!


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

    是的,Hans Boehm在N1528中为此提供了一个基本原理:为什么对于无限循环的未定义行为?尽管这是WG14文档的基本原理同样适用于C ++,并且该文档同时指向WG14和WG21:

    正如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指向一个非循环列表,我认为这是非常多的主流编译器的能力,并且如果没有整个程序信息通常是不可能的。

    非终止循环施加的限制是对编译器无法证明终止的终止循环的优化以及对实际非终止循环的优化的限制。 前者比后者更普遍,而且通常对优化更有意思。

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

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

    要么

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

    似乎并不重要。 (在前一种情况下,需要一些基本的数字理论来证明终止,在后一种情况下,我们需要了解j的可能值,以便这样做。对无符号整数进行换行可能会使一些推理复杂化。 )

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

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


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

    这是为了允许编译器转换,例如删除空循环,即使无法证实终止。

    据推测,这是因为证明终止在机械上是困难的,并且无法证明终止阻碍编译器,否则这些编译器可能进行有用的转换,例如将非依赖操作从循环前移到后循环,反之亦然,在一个线程中执行后循环操作循环在另一个循环中执行,依此类推。 如果没有这些转换,循环可能会阻止所有其他线程,同时等待一个线程完成所述循环。 (我使用“线程”松散地表示任何形式的并行处理,包括单独的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不确定”。


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

    我不会说这是特别直观的行为,但是这种解释比另一种解释更有意义,即编译器被任意允许在不调用UB的情况下忽略无限循环。

    如果无限循环是UB,则意味着非终止程序不被视为有意义的:根据C ++ 0x,它们没有语义。

    这确实也有一定的意义。 它们是一种特殊情况,其中一些副作用不再发生(例如,没有从main返回的东西),并且由于必须保留无限循环而妨碍了许多编译器优化。 例如,如果循环没有副作用,则在循环中移动计算是完全有效的,因为最终计算将在任何情况下执行。 但是如果循环从不终止,我们不能安全地重新排列代码,因为我们可能只是在程序挂起之前更改哪些操作实际执行。 除非我们将挂起的程序视为UB,否则。

    链接地址: http://www.djcxy.com/p/5197.html

    上一篇: Optimizing away a "while(1);" in C++0x

    下一篇: Resource placement (optimal strategy)