【问题标题】:which is more efficient? repetitive assignment or repetitive checking哪个更有效率?重复分配或重复检查
【发布时间】:2014-04-13 05:35:57
【问题描述】:

如果我有一个循环来检查数组中的特定值,并且由于某种原因必须遍历所有元素并且不能中途中断。

以下哪项会更有效:在每场比赛中盲目设置标志,或在设置标志之前检查标志是否为假。

bool happened = false;
while (...) {
  if (...) {
    happened = true;
  }
}

bool happened = false;
while (...) {
  if (...) {
    if (!happened) happened = true;
  }
}

据我所知,假设内存读取与内存写入一样快(忽略第二个示例中的额外指令),两者或多或少是等效的。我的结论正确吗?

【问题讨论】:

  • 根据您的问题的措辞方式,很难准确说出您要问什么,但我认为这将取决于您的访问模式。对数组进行迭代可能会很昂贵,但分支预测错误也是如此。
  • 我怀疑它会有所作为,只是支票看起来有点傻。
  • 虽然没有分析就不能说“效率”和任何真正的性能差异,但第一个在理论上似乎更好。因为当您决定将 happened 设置为 true 时,您不必担心它的当前值,并且额外检查(在第 2 次中)似乎有点矫枉过正。

标签: c performance optimization processing-efficiency


【解决方案1】:

到目前为止,大多数答案似乎都是“视情况而定”,但我认为这很明显:
如果你有条件地设置一个值,如果它已经不是那个东西,那么它在逻辑上与无条件地设置值相同。如果幸运的话,编译器会注意到并同等对待两者,但如果没有,那么无条件版本每次都会获胜。
1:它使用更少的指令
2:额外的指令是有条件的,影响分支预测

如果你要和

 if (cond) varx = vary;

编译器使用一个条件分支(如果硬件支持,可能是条件移动而不是分支)

如果你要去

 if (cond && varx != vary) varx = vary;

编译器将简化为第一种情况,或使用两个条件跳转(或一个跳转和一个条件移动)。

【讨论】:

    【解决方案2】:

    如果您使用任何有意义的优化,编译器将为您做出决定。写任何最干净和有意义的东西。对我来说,这将是第一个,因为它的代码更少并且不会引入更多的代码路径。为了好玩,我在 Clang 3.4 -O3 中做了一些测试:

    bool happened = false;
    extern volatile int dontOptMe1, dontOptMe2, dontOptMe3;
    while (dontOptMe1) {
      if (dontOptMe2) {
        happened = true;
      }
    }
    dontOptMe3 = happened;
    

    bool happened = false;
    extern volatile int dontOptMe1, dontOptMe2, dontOptMe3;
    while (dontOptMe1) {
      if (dontOptMe2) {
        if(!happened) happened = true;
      }
    }
    dontOptMe3 = happened;
    

    在伪 ASM 中导致以下结果:

      MOV      happened, 0
      BRA      LOOP_END
    LOOP_START:
      SELECTEQ dontOptMe2, 0, happened, happened, 1
    LOOP_END:
      BCZC     dontOptMe1, LOOP_START
    EXIT:
      STORE    dontOptMe3, happened
    

      MOV      happened, 0
      BCZS     dontOptMe1, EXIT
    LOOP:
      SELECTNE dontOptMe2, 0, R2, 1, 0
      SELECTEQ happened, 0, R3, 1, 0
      AND      R3, R2, R3
      SELECTNE R3, 0, happened, 1, 0
      BCZC     dontOptMe1, LOOP
    EXIT:
      STORE    dontOptMe3, happened
    

    第一个更可取。这也是 volatile 类型的限制性的一个很好的例子。我很惊讶编译器无法将第二个转换为第一个。

    注意:SELECTXX 表示,如果 Arg1 减去 Arg2 设置条件代码 XX,则将 Arg3 设置为 Arg4,否则将 Arg3 设置为 Arg5。所以:SELECTNE dontOptMe2, 0, R2, 1, 0 相当于:C 中的R2 = (dontOptMe2 == 0) ? 1 : 0;

    【讨论】:

      【解决方案3】:

      我已经在以下代码上凭经验运行它:

      bool happened = false;
      for (int i = 0; i < 1000000000; i++) {
        if (i % 2) {
          // uncomment the one you want to use
          // happened = true;
          // if (!happened) happened = true;
        }
      }
      

      我每次运行 10 次,第一次平均为 2.304 秒,标准差为 0.013 秒,第二次平均为 2.399 秒,标准差为 0.007 秒。两样本 t 检验表明 happened = true;if (!happened) happened = true; 更快,并且在 p = 7e-12 时差异具有统计学意义。

      【讨论】:

      • 这个例子似乎过于简化了。如果您正在分析优化的代码,任何体面的编译器都会优化发生的变量(并且很可能会优化整个循环)。分析未优化的代码没有意义。
      • 我使用-O0 编译,并检查了程序集以确保一切都在那里,因此我可以确认我正在运行未优化的代码。我不同意你的观点,即分析未优化的代码没有意义——这两个二进制文件是相同的,除了最内层循环中发生的事情。所以唯一改变的是我们正在比较的东西。
      【解决方案4】:

      一般来说,第一个版本对流水线更友好,因为它的指令流受到跳转的干扰更少,因此效率更高。但这取决于具体的架构特性和编译器优化。

      我相信这两个版本的性能差异在实际情况下并不明显。

      【讨论】:

        【解决方案5】:

        是的,你说的很对。它们或多或少是一样的。

        确切地说,如果“发生”经常发生,则第一种形式可能更可取,而如果“发生”很少发生,则第二种形式可能更受欢迎。但即便如此,我也不认为第二个更好。

        最后,它可能是一个微优化,它更好地追求可读性而不是性能。

        【讨论】:

          【解决方案6】:

          只有分析才能确定,但​​很可能变量无论如何都存储在寄存器中,而不是内存中(除非循环中有许多其他局部变量)并且根本不会有可测量的差异。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-02-23
            • 2021-11-25
            • 2010-11-01
            • 2013-08-06
            • 2011-05-20
            • 2014-10-31
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多