优化"while(1);" 在C++ 0x中

Joh*_*itb 150 c++ optimization loops c++11

更新,见下文!

我听说并读过C++ 0x允许编译器为以下代码段打印"Hello"

#include <iostream>

int main() {
  while(1) 
    ;
  std::cout << "Hello" << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

它显然与线程和优化功能有关.在我看来,这可能让许多人感到惊讶.

有人能够很好地解释为什么必须允许这样做吗?作为参考,最新的C++ 0x草案说明了6.5/5

在for语句的情况下,在for-init语句之外的循环,

  • 不调用库I/O函数,和
  • 不访问或修改易失性对象,以及
  • 不执行同步操作(1.10)或原子操作(第29条)

可以通过实现来假设终止.[注意:这是为了允许编译器转换,例如删除空循环,即使无法证明终止也是如此. - 结束说明]

编辑:

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

不幸的是,没有使用"未定义的行为".但是,只要标准说"编译器可以假设P",就暗示具有非-P属性的程序具有未定义的语义.

这是正确的,是否允许编译器为上述程序打印"Bye"?


这里有一个更有见地的线索,这是关于C的类似改变,由Guy完成上述链接文章开始.在其他有用的事实中,他们提出了一个似乎也适用于C++ 0x的解决方案(更新:这将不再适用于n3225 - 见下文!)

endless:
  goto endless;
Run Code Online (Sandbox Code Playgroud)

看来,编译器不允许优化它,因为它不是循环,而是跳转.另一个人总结了C++ 0x和C201X的建议更改

通过编写一个循环,程序员断言或者环路不可见的东西的行为(执行I/O,访问volatile对象,或执行同步或原子操作), 或者,它最终会终止.如果我通过编写一个没有副作用的无限循环来违反这个假设,我对编译器撒谎,而我的程序的行为是未定义的.(如果我很幸运,编译器可能会警告我.)语言不提供(不再提供?)表达无可见行为的无限循环的方法.


2011年3月3日更新为n3225:委员会将案文移至1.10/24并说

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

  • 终止,
  • 调用库I/O函数,
  • 访问或修改易失性对象,或
  • 执行同步操作或原子操作.

goto把戏,工作了!

Phi*_*ter 47

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

这旨在允许编译器转换,例如删除空循环,即使无法证明终止也是如此.

据推测,这是因为机械地证明终止是困难的,并且无法证明终止妨碍了编译器,否则编译器可以进行有用的转换,例如将非依赖操作从循环之前移动到之后或反之,在一个线程中执行循环后操作循环在另一个循环中执行,依此类推.如果没有这些转换,循环可能会在等待一个线程完成所述循环时阻塞所有其他线程.(我使用"thread"松散地表示任何形式的并行处理,包括单独的VLIW指令流.)

编辑:愚蠢的例子:

while (complicated_condition()) {
    x = complicated_but_externally_invisible_operation(x);
}
complex_io_operation();
cout << "Results:" << endl;
cout << x << endl;
Run Code Online (Sandbox Code Playgroud)

在这里,一个线程执行的速度会更快,而另一个线程complex_io_operation在循环中执行所有复杂的计算.但是如果没有你引用的子句,编译器必须先证明两件事才能进行优化:1)complex_io_operation()不依赖于循环的结果,2)循环将终止.证明1)非常简单,证明2)是停止问题.使用该子句,它可以假设循环终止并获得并行化胜利.

我还想象设计人员认为生产代码中出现无限循环的情况非常罕见,通常是事件驱动的循环,它以某种方式访问​​I/O. 结果,他们对罕见的情况(无限循环)进行了极度优化,以支持优化更常见的情况(非无限期,但难以机械地证明非无限循环).

但是,它确实意味着学习示例中使用的无限循环会因此受到影响,并且会在初学者代码中引发陷阱.我不能说这完全是件好事.

编辑:关于你现在链接的富有洞察力的文章,我会说"编译器可能假定X关于程序"在逻辑上等同于"如果程序不满足X,则行为未定义".我们可以这样说明:假设存在一个不满足属性X的程序.该程序的行为将在何处定义?标准仅定义假设属性X为真的行为.尽管标准没有明确声明行为未定义,但它已经通过省略声明它未定义.

考虑一个类似的论点:"编译器可能假设变量x仅在序列点之间最多分配一次"等同于"在序列点之间多次分配x未定义".


Sha*_*our 30

有人能够很好地解释为什么必须允许这样做吗?

是的,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;
}
Run Code Online (Sandbox Code Playgroud)

这两个循环可以合并并由以下循环替换吗?

for (p = q; p != 0; p = p -> next) {
        ++count;
        ++count2;
}
Run Code Online (Sandbox Code Playgroud)

如果没有6.8.5p6中针对无限循环的特殊分配,则不允许这样做:如果第一个循环没有因为q指向循环列表而终止,则原始的永远不会写入count2.因此,它可以与访问或更新count2的另一个线程并行运行.对于转换版本,尽管存在无限循环,但它不再安全.因此,转型可能引入数据竞争.

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

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

显然还存在具有整数循环变量的for循环,其中编译器难以证明终止,因此编译器难以在没有6.8.5p6的情况下重构循环.甚至像

for (i = 1; i != 15; i += 2)
Run Code Online (Sandbox Code Playgroud)

要么

for (i = 1; i <= 10; i += j)
Run Code Online (Sandbox Code Playgroud)

处理起来似乎很不重要.(在前一种情况下,需要一些基本的数论来证明终止,在后一种情况下,我们需要知道j的可能值这样做.无符号整数的环绕可能会进一步使一些推理复杂化. )

这个问题似乎适用于几乎所有的循环重组转换,包括编译器并行化和缓存优化转换,这两种转换都可能在重要性上获得,并且对于数字代码已经非常重要.为了能够以最自然的方式编写无限循环,这似乎变成了可观的成本,特别是因为我们大多数人很少有意无限地编写无限循环.

与C的一个主要区别是C11为控制表达式提供了一个例外,这些表达式是与C++不同的常量表达式,并使您的特定示例在C11中得到明确定义.


jal*_*alf 14

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

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

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

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

  • @Donal:我从来没有在图灵机中说过它的语义.我们正在讨论C++*中没有副作用*的无限循环的语义.当我读到它时,C++ 0x选择说这些循环是未定义的. (10认同)
  • "空无限循环是未定义的行为"?阿兰·图灵会乞求不同,但只有当他在坟墓里过度旋转时才会这样. (6认同)
  • 这是否意味着 C++0x 不适合嵌入式设备?几乎所有嵌入式设备都是非终止的,并且在一个大的“while(1){...}”中完成它们的工作。他们甚至经常使用“while(1);”来调用看门狗辅助重置。 (4认同)
  • @vsz:第一种形式很好。无限循环是完美定义的,只要它们具有某种可观察的行为。第二种形式更棘手,但我可以想到两种非常简单的方法:(1)针对嵌入式设备的编译器可以选择在这种情况下定义更严格的行为,或者(2)您创建一个调用一些虚拟库函数的主体. 只要编译器不知道该函数做什么,它就必须假设它可能有一些副作用,因此它不能与循环混淆。 (2认同)

lin*_*r27 8

我认为这与这类问题相似,它引用了另一个线程.优化可以偶尔删除空循环.

  • 好问题.好像那个人确实存在这个段落允许编译器导致的问题.在其中一个答案的链接讨论中,写道*"不幸的是,未使用"未定义行为"这两个词.但是,无论何时标准上说"编译器可能假设为P",都暗示一个程序具有属性not-P具有未定义的语义."*.这让我感到惊讶.这是否意味着我上面的示例程序具有未定义的行为,并且可能只是突然出现错误? (3认同)

Dan*_*wby 8

相关问题是允许编译器重新排序副作用不冲突的代码.即使编译器为无限循环生成非终止机器代码,也可能出现令人惊讶的执行顺序.

我相信这是正确的方法.语言规范定义了强制执行顺序的方法.如果你想要一个无法重新排序的无限循环,请写下:

volatile int dummy_side_effect;

while (1) {
    dummy_side_effect = 0;
}

printf("Never prints.\n");
Run Code Online (Sandbox Code Playgroud)

  • @ JohannesSchaub-litb:如果循环(无论是否无限)在执行过程中不读取或写入任何易失性变量,并且不调用任何可能这样做的函数,则编译器可以自由地将循环的任何部分推迟到首先尝试访问其中计算的内容。给定`unsigned int dummy; while(1){dummy ++;} fprintf(stderror,“ Hey \ r \ n”); fprintf(stderror,“结果为%u \ r \ n”,dummy);`,第一个`fprintf`可以执行,但是第二个不能执行(编译器可以在两个`fprintf'之间移动'dummy'的计算。 ,但不能超过显示其值的那一个)。 (2认同)