以下代码通过指数缓慢的算法计算斐波纳契数:
#include <cstdlib>
#include <iostream>
#define DEBUG(var) { std::cout << #var << ": " << (var) << std::endl; }
constexpr auto fib(const size_t n) -> long long
{
return n < 2 ? 1: fib(n - 1) + fib(n - 2);
}
int main(int argc, char *argv[])
{
const long long fib91 = fib(91);
DEBUG( fib91 );
DEBUG( fib(45) );
return EXIT_SUCCESS;
}
我在运行时计算第45个Fibonacci数,在编译时计算第91个Fibonacci数。
有趣的是,GCC 4.9编译代码并在几分之一秒内计算fib91
,但需要一段时间才能吐出fib(45)
。
我的问题:如果GCC足够聪明以优化fib(91)
计算而不采用指数缓慢的路径,那么是什么阻止它为fib(45)
做同样的事情?
以上是否意味着GCC产生了fib
函数的两个编译版本,其中一个是快速的而另一个是指数级的慢?
问题是不编译器如何优化fib(91)
计算(是的!它确实使用了一种记忆),但是如果它知道如何优化fib
函数,为什么fib(45)
没有做同样的事情?并且,fib
函数有两个单独的汇编吗?一个慢,另一个快?
答案 0 :(得分:38)
GCC可能会记住constexpr
个函数(启用fib(n)
的Θ(n)计算。这对编译器来说是安全的,因为constexpr
函数纯粹是功能性的。
将Θ(n)“编译器算法”(使用memoization)与你的Θ(φ n )运行时算法(其中φ是黄金比率)进行比较,突然之间它完全合理编译器要快得多。
来自the constexpr
page on cppreference(强调补充):
constexpr说明符声明在编译时评估函数或变量的值是可能。
constexpr
说明符不声明必需在编译时评估函数或变量的值。因此,人们只能猜测GCC正在使用什么启发式来选择是在编译时还是在语言规则不需要编译时计算的运行时进行评估。它可以根据具体情况选择,但仍然是正确的。
如果你想强制编译器在编译时评估你的constexpr
函数,这里有一个简单的技巧。
constexpr auto compute_fib(const size_t n) -> long long
{
return n < 2 ? n : compute_fib(n - 1) + compute_fib(n - 2);
}
template <std::size_t N>
struct fib
{
static_assert(N >= 0, "N must be nonnegative.");
static const long long value = compute_fib(N);
};
在其余代码中,您可以访问fib<45>::value
或fib<91>::value
,并保证在编译时对其进行评估。
答案 1 :(得分:16)
在编译时,编译器可以memoize函数的结果。这是安全的,因为该函数是constexpr,因此将始终返回相同输入的相同结果。
在运行时,它理论上可以做同样的事情。但是,大多数C ++程序员都会对优化传递感到不满,导致隐藏的内存分配。
答案 2 :(得分:2)
当你要求fib(91)在源代码中给你的const fib91赋值时,编译器被迫从你的const expr计算该值。它没有编译函数(你似乎认为),只是它看到计算fib91它需要fib(90)和fib(89),计算它需要fib(87)......所以直到他计算fib(1)给出。这是一个$ O(n)$算法,结果计算得足够快。
但是当你要求在运行时评估fib(45)时,编译器必须选择使用实际的函数调用或预先计算结果。最终它决定使用已编译的函数。现在,编译的函数必须完全执行指数算法,您已经确定编译器无法实现memoization来优化递归函数(考虑需要分配一些缓存并了解要保留多少值以及如何管理它们在函数调用之间。)