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

更新,见下文!

我听说过,C ++ 0x允许编译器在下面的代码片段中打印“Hello”

#include <iostream> int main() { while(1) ; std::cout << "Hello" << std::endl; } 

这显然与线程和优化function有关。 在我看来,这可以让很多人感到惊讶。

有人有一个很好的解释,为什么这是必要的允许? 作为参考,最新的C ++ 0x草案在6.5/5

一个循环,在for语句的for-init语句之外,

  • 不会调用库I / O函数,并且
  • 不访问或修改易失性对象,
  • 不执行同步操作(1.10)或primefaces操作(第29章)

可能由执行方式假定终止。 [注意:这是为了允许编译器转换,例如删除空循环,即使在无法证实终止的情况下也是如此。 – 结束注意]

编辑:

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

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

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


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

 endless: goto endless; 

编译器不允许优化,看来,因为它不是一个循环,而是一个跳转。 另一个家伙总结了C ++ 0x和C201X的改进build议

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


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

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

  • 终止,
  • 拨打一个库的I / Ofunction,
  • 访问或修改易失性对象,或
  • 执行同步操作或primefaces操作。

goto技巧将不再工作了!

有人有一个很好的解释,为什么这是必要的允许?

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

正如N1509指出的那样,当前的草案在6.8.5p6中给出了无限循环的无限循环。 这样做的一个主要问题是,它允许代码跨越可能不终止的循环。 例如,假设我们有以下循环,其中count和count2是全局variables(或者已经取得了它们的地址),而p是一个局部variables,其地址未被采用:

 for (p = q; p != 0; p = p -> next) { ++count; } for (p = q; p != 0; p = p -> next) { ++count2; } 

可以将这两个循环合并并replace为以下循环吗?

 for (p = q; p != 0; p = p -> next) { ++count; ++count2; } 

如果没有在6.8.5p6中对无限循环的特殊configuration,这将是不允许的:如果第一个循环没有终止,因为q指向一个循环列表,那么原始logging不会写入到count2。 因此它可以与访问或更新count2的另一个线程并行运行。 尽pipe存在无限循环,但对于访问count2的转换版本来说,这已经不再安全。 因此,转换可能引入数据竞赛。

在这种情况下,编译器不太可能certificate循环终止; 它必须明白,q指向一个非循环列表,我相信这是超出了大多数主stream编译器的能力,并且通常不可能没有整个程序信息。

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

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

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

要么

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

似乎是不平凡的处理。 (在前一种情况下,需要一些基本的数论来certificate终止,在后一种情况下,我们需要了解j的可能值的一些事情,无符号整数的环绕可能会使这个推理复杂化。 )

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

与C的一个主要区别是C11提供了一个例外,用于控制与C ++不同的常量expression式 ,并使您在C11中定义明确的示例。

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

这样做的目的是允许编译器转换,例如清除空循环,即使无法证实终止。

据推测,这是因为certificate终止机制是困难的 ,并且无法certificate终止妨碍编译器可能进行有用的转换,例如将非依赖操作从循环前移到后循环,反之亦然,在一个线程中执行后循环操作循环在另一个执行,依此类推。 没有这些转换,循环可能会阻塞所有其他线程,而等待一个线程完成所述循环。 (我使用“线程”松散地表示任何forms的并行处理,包括单独的VLIW指令stream。)

编辑:哑例如:

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

在这里,一个线程执行complex_io_operation会更快,而另一个执行循环中所有复杂的计算。 但是如果没有你引用的子句,编译器在做优化之前必须certificate两件事情:1) complex_io_operation()不依赖于循环的结果; 2) 循环终止 。 certificate1)很容易,certificate2)是停止问题。 通过这个条款,它可以假定循环终止并且获得一个并行的胜利。

我也想象一下,devise者认为在生产代码中出现无限循环的情况非常罕见,通常是事件驱动的循环,以某种方式访问​​I / O。 因此,他们对罕见的情况(无限循环)表示了赞同,以优化更常见的情况(非无限的,但难以机械地certificate非无限循环)。

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

编辑:关于你现在链接的有洞察力的文章,我会说“编译器可能假设X关于程序”在逻辑上等同于“如果程序不满足X,行为是未定义的”。 我们可以这样表示:假设存在一个不满足属性X的程序,那么这个程序的行为在哪里被定义? 标准只定义假设属性X为真的行为。 尽pipe标准没有明确地声明未定义的行为,但它已经通过省略声明了它的未定义。

考虑一个类似的论点:“编译器可能假设一个variablesx最多只在一个序列点之间分配一次”相当于“在序列点之间不止一次赋值x是未定义的”。

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

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

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

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

我认为这是沿着这种types的问题 ,其中引用另一个线程的线 。 优化可以偶尔删除空循环。

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

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

 volatile int dummy_side_effect; while (1) { dummy_side_effect = 0; } printf("Never prints.\n"); 

我认为这个问题可能是最好的陈述,因为“如果后面的一段代码不依赖于较早的一段代码,并且较早的一段代码对系统的任何其他部分没有副作用,编译器的输出可以在执行前一个代码之前,之后或混合执行后一段代码,即使前者包含循环, 而不考虑前一代码是否实际完成,例如,编译器可以重写:

 void testfermat(int n)
 {
   int a = 1,b = 1,c = 1;
   while(pow(a,n)+ pow(b,n)!= pow(c,n))
   {
    如果(b> a)a ++; 否则if(c> b){a = 1; 基础B ++};  else {a = 1;  B = 1;  C ++};
   }
   printf(“结果是”);
   printf(“%d /%d /%d”,a,b,c);
 }

 void testfermat(int n)
 {
   if(fork_is_first_thread())
   {
     int a = 1,b = 1,c = 1;
     while(pow(a,n)+ pow(b,n)!= pow(c,n))
     {
      如果(b> a)a ++; 否则if(c> b){a = 1; 基础B ++};  else {a = 1;  B = 1;  C ++};
     }
     signal_other_thread_and_die();
   }
  其他//第二个线程
   {
     printf(“结果是”);
     wait_for_other_thread();
   }
   printf(“%d /%d /%d”,a,b,c);
 }

一般来说不是无理的,虽然我可能会担心:

   int total = 0;
   for(i = 0; num_reps> i; i ++)
   {
     update_progress_bar(ⅰ);
    总+ = do_something_slow_with_no_side_effects(ⅰ);
   }
   show_result(总);

会成为

   int total = 0;
   if(fork_is_first_thread())
   {
     for(i = 0; num_reps> i; i ++)
      总+ = do_something_slow_with_no_side_effects(ⅰ);
     signal_other_thread_and_die();
   }
  其他
   {
     for(i = 0; num_reps> i; i ++)
       update_progress_bar(ⅰ);
     wait_for_other_thread();
   }
   show_result(总);

通过让一个CPU处理计算,另一个CPU处理进度条更新,重写将提高效率。 不幸的是,这会使进度条更新而不是比他们应该更有用。

我认为值得指出的是,除了通过非易失性非同步variables与其他线程交互的事实之外,循环将是无限的,现在可以通过新的编译器产生不正确的行为。

换句话说,使你的全局variables易变 – 以及通过指针/引用传递给这样一个循环的参数。

如果它是一个无限循环的话,对于非平凡的情况,编译器是不可判定的。

在不同的情况下,优化器会为你的代码达到一个更好的复杂类(例如,它是O(n ^ 2),你可以在优化之后得到O(n)或O(1))。

所以,要包含一个不允许将无限循环移除到C ++标准的规则将使得很多优化成为不可能的。 而大多数人不想要这个。 我认为这相当回答你的问题。


另一件事:我从来没有见过任何有效的例子,你需要一个无所事事的无限循环。

我听说过的一个例子是一个真正应该被解决的丑陋的黑客:它是关于embedded式系统的唯一方法来触发重置是冻结设备,使看门狗自动重新启动它。

如果你知道任何有效的/好的例子,你需要一个什么都不做的无限循环,请告诉我。