我在编译时写了程序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&lt; 40&gt;,fibonacci&lt; 39&gt; ...&斐波纳契℃,GT)。
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
。
答案 0 :(得分:13)
试试这个:
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
的零时间内运行。你的常规&#34;方法编译大约0.4s,运行0.8s。只是为了检查,结果是102334155
对吗?
当我尝试自己的constexpr
解决方案时,编译器会运行几分钟,然后我停止了它,因为显然内存已满(计算机开始冻结)。编译器试图计算最终结果,并且您的实现在编译时使用效率极低。
使用此解决方案,N-2
,N-1
处的模板实例化在实例化N
时会被重复使用。因此,fibonacci<40>
实际上在编译时被称为值,并且在运行时无需执行任何操作。这是一种动态编程方法,当然,如果您在0
计算之前将所有值存储在N-1
到N
,则可以在运行时执行相同操作。
使用您的解决方案,编译器可以在编译时评估fibonacci<N>()
但不需要。在您的情况下,全部或部分计算留给运行时。就我而言,所有计算都是在编译时尝试的,因此永远不会结束。
答案 1 :(得分:5)
原因是您的运行时解决方案不是最佳的。对于每个fib数,函数被调用几次。斐波那契序列具有重叠的子问题,例如fib(6)
调用fib(4)
,fib(5)
也调用fib(4)
。
基于模板的方法,使用(无意中)动态编程方法,意味着它存储先前计算的数字的值,避免重复。因此,当fib(5)
调用fib(4)
时,该数字已经在fib(6)
执行时计算出来。
我建议查找“动态编程斐波那契”并尝试这一点,它应该会大大加快速度。
答案 2 :(得分:0)
将-O1(或更高版本)添加到GCC4.8.1将使fibonacci&lt; 40&gt;()成为编译时常量,并且所有模板生成的代码将从程序集中消失。以下代码
int foo()
{
return fibonacci<40>();
}
将导致程序集输出
foo():
movl $102334155, %eax
ret
这提供了最佳的运行时性能。
但是,看起来你在没有优化的情况下构建(-O0),所以你会得到一些不同的东西。 40个斐波那契函数中的每一个的汇编输出看起来基本相同(除了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(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并采取适当的行动。模板版本中不需要进行此比较,因为它通过模板的魔力内置到函数中。我的猜测是模板代码的未优化版本更快,因为你避免了那些比较,也没有任何错过的分支预测。
答案 3 :(得分:-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在他的“计算机编程艺术”第一部分中描述的一个想法。我无法记住本书中的确切位置,但我确信在那里描述了算法。