在C++11中计算编译时的斐波那契数(递归方法)(constexpr)

Calculate the Fibonacci number (recursive approach) in compile time (constexpr) in C++11

本文关键字:递归方法 constexpr C++11 计算 编译      更新时间:2023-10-16

我在编译时编写了Fibonacci数计算程序(constexpr(使用C++11中支持的模板元编程技术的问题。目的其中之一是计算模板元编程方法和旧的传统方法在运行时的差异。

// Template Metaprograming Approach
template<int  N>
constexpr int fibonacci() {return fibonacci<N-1>() + fibonacci<N-2>(); }
template<>
constexpr int fibonacci<1>() { return 1; }
template<>
constexpr int fibonacci<0>() { return 0; }

// Conventional Approach
 int fibonacci(int N) {
   if ( N == 0 ) return 0;
   else if ( N == 1 ) return 1;
   else
      return (fibonacci(N-1) + fibonacci(N-2));
} 

我在GNU/Linux系统上为N=40运行了两个程序,并测量了时间和发现传统解决方案(1.15秒(比基于模板的解决方案(0.55秒(慢大约两倍。这是一个显著的改进,因为这两种方法都是基于递归的。

为了更好地理解它,我用g++编译了程序(-fdump-tree all标志(,发现编译器实际上生成了40个不同的函数(如fibonacci<40>、fibonacci<39>…fibonacci-lt;0>(

constexpr int fibonacci() [with int N = 40] () {
  int D.29948, D.29949, D.29950;
  D.29949 = fibonacci<39> ();
  D.29950 = fibonacci<38> ();
  D.29948 = D.29949 + D.29950;
  return D.29948;
}
constexpr int fibonacci() [with int N = 39] () {
  int D.29952, D.29953, D.29954;
  D.29953 = fibonacci<38> ();
  D.29954 = fibonacci<37> ();
  D.29952 = D.29953 + D.29954;
  return D.29952;
}
...
...
...
constexpr int fibonacci() [with int N = 0] () {
  int D.29962;
  D.29962 = 0;
  return D.29962;
}

我还在GDB中调试了程序,发现上面所有的函数都是执行的次数与传统递归方法相同。如果两个版本的程序执行函数的次数相等(递归(,那么如何通过模板元编程技术实现这一点?我还想知道你对基于模板元编程的方法与其他版本相比如何以及为什么花费一半时间的看法?这个程序能比现在的更快吗?

基本上,我在这里的意图是尽可能多地了解内部发生了什么。

我的机器是带有GCC 4.8.1的GNU/Linux,我对这两个程序都使用了优化-o3

试试这个:

template<size_t N>
struct fibonacci : integral_constant<size_t, fibonacci<N-1>{} + fibonacci<N-2>{}> {};
template<> struct fibonacci<1> : integral_constant<size_t,1> {};
template<> struct fibonacci<0> : integral_constant<size_t,0> {};

对于clang和-Os,它大约在0.5秒内编译,并在N=40时间内运行。你的"传统"方法大约在0.4秒内编译,在0.8秒内运行。只是为了检查一下,结果是102334155,对吗?

当我尝试您自己的constexpr解决方案时,编译器运行了几分钟,然后我停止了它,因为显然内存已满(计算机开始冻结(。编译器试图计算最终结果,而您的实现在编译时使用效率极低。

使用此解决方案,在实例化N时,会重复使用N-2N-1处的模板实例化。所以fibonacci<40>在编译时实际上是一个已知的值,在运行时无需执行任何操作。这是一种动态编程方法,当然,如果在N计算之前将所有值存储在0N-1中,则可以在运行时执行同样的操作。

使用您的解决方案,编译器可以在编译时评估fibonacci<N>(),但不需要。在您的情况下,全部或部分计算留给运行时使用。在我的例子中,所有的计算都是在编译时尝试的,因此永远不会结束。

原因是您的运行时解决方案不是最佳的。对于每个fib数,函数都会被调用多次。斐波那契序列有重叠的子问题,例如fib(6)调用fib(4)fib(5)也调用fib(4)

基于模板的方法(无意中(使用了动态编程方法,这意味着它存储以前计算的数字的值,避免重复。因此,当fib(5)调用fib(4)时,这个数字已经在fib(6)调用时计算出来了。

我建议查找"动态编程fibonacci"并尝试一下,它应该会大大加快速度。

在GCC4.8.1中添加-O1(或更高版本(将使fibonacci<40>((一个编译时常数,所有模板生成的代码都将从程序集中消失。以下代码

int foo()
{
  return fibonacci<40>();
}

将导致组件输出

foo():
    movl    $102334155, %eax
    ret

这提供了最佳的运行时性能。

然而,看起来您在构建时没有进行优化(-O0(,所以您得到了一些完全不同的东西。40个fibonacci函数中每个函数的汇编输出看起来基本相同(除了0和1的情况(

int fibonacci<40>():
    pushq   %rbp
    movq    %rsp, %rbp
    pushq   %rbx
    subq    $8, %rsp
    call    int fibonacci<39>()
    movl    %eax, %ebx
    call    int fibonacci<38>()
    addl    %ebx, %eax
    addq    $8, %rsp
    popq    %rbx
    popq    %rbp
    ret

这是直接的,它设置堆栈,调用另外两个fibonacci函数,添加值,拆下堆栈,然后返回。没有分支,也没有比较。

现在将其与传统方法的组件进行比较

fibonacci(int):
    pushq   %rbp
    pushq   %rbx
    subq    $8, %rsp
    movl    %edi, %ebx
    movl    $0, %eax
    testl   %edi, %edi
    je  .L2
    movb    $1, %al
    cmpl    $1, %edi
    je  .L2
    leal    -1(%rdi), %edi
    call    fibonacci(int)
    movl    %eax, %ebp
    leal    -2(%rbx), %edi
    call    fibonacci(int)
    addl    %ebp, %eax
    .L2:
    addq    $8, %rsp
    popq    %rbx
    popq    %rbp
    ret

每次调用函数时,都需要检查N是0还是1,并采取适当的操作。在模板版本中不需要这种比较,因为它是通过模板的魔力构建到函数中的。我的猜测是,模板代码的未优化版本更快,因为您可以避免这些比较,也不会有任何遗漏的分支预测。

也许只需要使用更高效的算法?

constexpr pair<double, double> helper(size_t n, const pair<double, double>& g)
{
    return n % 2
        ? make_pair(g.second * g.second + g.first * g.first, g.second * g.second + 2 * g.first * g.second)
        : make_pair(2 * g.first * g.second - g.first * g.first, g.second * g.second + g.first * g.first);
}
constexpr pair<double, double> fibonacciRecursive(size_t n)
{
    return n < 2
        ? make_pair<double, double>(n, 1)
        : helper(n, fibonacciRecursive(n / 2));
}
constexpr double fibonacci(size_t n)
{
    return fibonacciRecursive(n).first;
}

我的代码是基于D.Knuth在他的"计算机编程艺术"第一部分中描述的一个想法。我记不清这本书的确切位置了,但我确信算法是在那里描述的。