C++ 编译器如何如此快速地评估递归 constexpr 函数?

ahs*_*jfk 51 c++ recursion function constexpr

我一直在学习 C++constexpr函数,我实现了一个constexpr递归函数来找到第 n 个斐波那契数。

#include <iostream>
#include <fstream>
#include <cmath>
#include <algorithm>
#include <vector>

constexpr long long fibonacci(int num) {
    if (num <= 2) return 1;
    return fibonacci(num - 1) + fibonacci(num - 2);
}

int main() {
    auto start = clock();
    long long num = fibonacci(70);
    auto duration = (clock() - start) / (CLOCKS_PER_SEC / 1000.);
    std::cout << num << "\n" << duration << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

如果我constexprfibonacci()函数中删除标识符,则fibonacci(70)需要很长时间来评估(超过 5 分钟)。然而,当我保持原样时,程序仍然在 3 秒内编译,并在不到0.1几毫秒的时间内打印出正确的结果。

我了解到constexpr函数是在编译时计算的,所以这意味着fibonacci(70)编译器在不到 3 秒的时间内计算出来!但是,C++ 编译器比 C++ 代码具有更好的计算性能似乎不太正确。

我的问题是,在我按下“构建”按钮和编译完成之间,C++ 编译器是否真的评估了函数?还是我误解了关键字constexpr

编辑:这个程序是g++ 7.5.0--std=c++17.

orl*_*rlp 57

constexpr函数没有副作用,因此可以放心地记忆。鉴于运行时的差异,最简单的解释是编译器在编译时记忆 constexpr 函数。这意味着fibonacci(n)对于 each 只计算一次n,并且所有其他递归调用都从查找表中返回。

  • 我不会对我的编译器引入一个不可见的、不可控的哈希表来在运行时记忆我的函数感到满意。不过,编译时的内存是“便宜的”,因此在评估 constexpr 时转换似乎是合理的。 (11认同)
  • 这可能是对 [as-if 规则](https://en.cppreference.com/w/cpp/language/as_if) 的一种发挥。如果编译器认为代码会更快并且不会改变可观察的行为,那么它可以对代码做任何它想做的事。当计算“constxepr”函数的结果时,编译器没有理由不能在内部执行此操作。有趣的问题是,如果它可以在编译时看到“constexpr”函数的这种优化,为什么它不能将相同的效果应用于运行时版本? (9认同)
  • @user4581301 - 对于为什么编译器会在编译时“看到”“constexpr”函数而不是“运行时版本”的优化有许多可能的解释。`constexpr` 允许编译器“假设”一些事情,但必须进行分析才能得出“运行时”版本的这些事情。该分析可能不会完成(可能会很昂贵),因此优化不会完成。相反,编译器可能会继续尝试计算“constexpr”函数,直到耗尽内存。编译器的执行质量取决于很多因素。 (7认同)
  • @ahskdjfk 正确。这是“constexpr”因其性质而允许的优化。然而,我不认为编译器“需要”进行此优化,因此您的情况可能会有所不同。 (4认同)
  • @orlp:正如我在上面的评论中提到的,不同版本的 gcc 绝对*不*应用这种优化(OP 7.5 这样做,最新的 10.2 版本不这样做,至少默认情况下不这样做),所以是的,绝对是里程不同的情况。 (2认同)
  • 没有副作用的 `constexpr` 函数对我来说是个新闻。您能指出标准中需要这样做的地方吗?似乎 [expr.const:5.16](https://eel.is/c++draft/expr.const#5.16) 中隐含着这一点:“表达式 E 是核心常量表达式,除非 E [ 的求值...] 将评估 [...] 对象的修改 [...] 除非它应用于引用非易失性对象的文字类型的非易失性左值,该对象的生命周期开始于 E 的评估”。IIUC,这排除了修改非常量表达式本地的对象,即副作用 (2认同)

twa*_*zyk 6

为其他人指出的内容添加一些细节:constexpr不必在运行时计算函数,并且可以影响它的参数之一是-fconstexpr-ops-limit.

在 GCC 10.2.0 上,-fconstexpr-ops-limit=1000000000(1B) 并fibonacci(40)生成预编译值,但如果将限制降低到 10000000 (10M),则函数将在运行时计算。如果要确保始终在编译时计算该值,则需要在函数之外标记long long num为。constexprfibonacci

注意:相反的例子是在编译时计算的非 constexpr 函数(优化后),并用 标记它__attribute__ ((const))可能有助于编译器做出这样的决定。但是,我的编译器没有优化它。

  • 注意:如果您使用“constexpr long long num”,则保证它不会在运行时计算。它不保证它会在编译时计算;它只是确保当编译器可能回退到运行时评估时,编译会失败。 (5认同)