【问题标题】:Why does compiler optimization not generate a loop for sum of integers from 1..N?为什么编译器优化不会为 1..N 的整数和生成循环?
【发布时间】:2022-01-09 00:34:15
【问题描述】:

为了更好地理解编译器,尤其是汇编语言,我一直在试验一段简单的代码,其中计算了第一个 N 数字的总和,结果应该是 N(N+1)/2N(N-1)/2

如代码所示,有两个功能:

#include <cstdint>


// Once compiled with optimization, the generated assembly has a loop

uint64_t sum1( uint64_t n ) {  
    uint64_t sum = 0;
    for ( uint64_t j=0; j<=n; ++j ) {
        sum += j;
    }
    return sum;
}

// Once compiled with optimization, the generated assembly of the following has no loop

uint64_t sum2( uint64_t n ) {  
    uint64_t sum = 0;
    for ( uint64_t j=0; j<n; ++j ) {
        sum += j;
    }
    return sum;
}

在第一个函数中,我从 O 循环到 N,即j&lt;=n,在第二个函数中,我从 O 到 N-1,即j&lt;n

我的理解/观察:

  • 对于第一个函数sum1,生成的程序集有一个循环,而对于第二个函数sum2,程序集没有循环。但是,一旦我删除了编译器优化,即-O3,那么你终于可以看到汇编中第二个函数的循环了。

  • 要查看编译器优化后生成的程序集,请参阅Optimized

  • 要查看未经编译器优化的生成程序集,请参阅此non-optimized

  • 编译器是 x86-64 clang

问题:为什么编译器优化不显示程序集中的另一个循环?

【问题讨论】:

  • 编译器似乎观察到sum1 可以永远循环,对于某个n
  • 编译器的(理想)目标是创建最高效的代码,其具有与源代码相同的可观察行为(优化时)。在循环中添加 temp 的输出以强制编译器保持循环。
  • 考虑sum1(std::numeric_limits&lt;uint64_t &gt;::max())
  • @SamVarshavchik:正如 user17732522 所指出的,编译器可能会假设循环终止;这是 C 或 C++ 标准授予的公理。这在逻辑上等同于假设 n != UINT64_MAX,因为这些都是循环终止的所有情况和唯一情况。所以这相当于说如果程序以n 等于UINT64_MAX 执行,则行为不是由C 或C++ 标准定义的。它没有被定义为无限循环。
  • @IgorTandetnik:这仅适用于 C++。在 C 中,它并不适用于所有循环;具有控制表达式的迭代语句是常量表达式(包括不存在的表达式,它隐式非零)可以无限继续而没有副作用。因此,您可以编写 OP 的循环并通过将 j &lt;= n 从控制表达式移动到条件 break; 来获得定义的无限循环行为而不是未定义的行为。

标签: c++ loops assembly clang compiler-optimization


【解决方案1】:

这是因为你的编译器非常非常聪明,它知道从 0 到 n 的所有值的总和可以用一个简单的数学公式来计算,而不是循环。

但是,您的 C++ 编译器还发现这个数学公式不能在 &lt;= 版本中使用,因为对于某些输入值会触发一个导致无限循环的错误,所以所有的赌注都被取消了,编译器编译代码与给出的完全相同。

【讨论】:

  • 绝对正确的是,在调试代码时,您不希望发生此类优化。这正是优化需要配置设置来启用(或禁用)的原因。
  • 如果你让无限循环更明显,clang会识别并移除它,即使使用sum中计算的值也是如此。它甚至会产生一个没有指令的空函数,这将导致无意义的程序:godbolt.org/z/cv6ndjYhT/z/Pff5rf1eh
  • @Jellyboy:线性序列和循环的模式识别器很可能正在寻找循环中的特定事物,这可能包括可证明是有限的循环。 clang/LLVM 的 this 部分未能使用来自__builtin_assume 的值范围信息这一事实很容易错过优化。虽然有趣的是,如果你使用 n &amp;= 0xfff 而不是假设,优化器能够使用高斯公式:godbolt.org/z/Mn16PYvqo(避免溢出的努力要少一些,但对于微小的 n 来说仍然比需要的多。)跨度>
  • @Jellyboy:这是显而易见的,也是迄今为止最有可能的解释。两个版本的一些实验都与该理论一致,例如j += 3 使 j&lt;n 版本未优化。有趣的是,使用__builtin_assume( n&lt;1000 ) 会破坏我们使用n&amp;=0xffff 获得的优化。 godbolt.org/z/5j3nhdnvT。无论__builtin_assume 如何,clang 都会发生完全空循环删除总是。 (GCC 不会这样做,尽管使用 if(!x)__builtin_unreachable() 的 ASSUME 确实让 GCC 也可以删除潜在的无限空循环。)godbolt.org/z/4TKfxq4rn
  • 是的,就是这样,我们应该期待好的推测,最好有足够的证据支持该理论。也是有用的实用建议,就是这样。期望回答者深入研究 LLVM 源代码并找到确切的模式识别器算法来排除任何其他可能性是不合理的。如果有人知道更准确的解释(或者更糟糕的是,如果解释完全错误,这在基于 CPU/缓存如何工作的性能问题上并不罕见),他们可以发表评论。但缺乏明确的证据并不是 IMO 的问题,至少在这里是这样。
猜你喜欢
  • 1970-01-01
  • 2016-07-26
  • 1970-01-01
  • 1970-01-01
  • 2012-07-01
  • 2015-09-15
  • 2014-09-28
  • 2013-03-01
  • 2021-02-17
相关资源
最近更新 更多