Вычислить число Фибоначчи (рекурсивный подход) во время компиляции (constexpr) в C++11
Я написал программу вычисления числа Фибоначчи во время компиляции (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));
}
я запустил обе программы для N = 40 на моей системе GNU / Linux и измерил время и найдено, что это обычное решение (1.15 секунда) вокруг в два раза медленнее, чем решение на основе шаблона (0,55 секунды). Это значительное улучшение, поскольку оба подхода основаны на рекурсии.
чтобы понять это больше, я скомпилировал программу (-fdump-дерево-все флаг) в g++ и обнаружил, что компилятор фактически сгенерировал 40 различных функций (например, Фибоначчи, Фибоначчи...Фибоначчи).
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 и обнаружил, что все вышеперечисленные функции выполнил равное количество раз, как и при обычном рекурсивном подходе. Если обе версии программы выполняют функцию равное количество раз (рекурсивно), то как это достигается методами метапрограммирования шаблонов? Я также хотел бы знать Ваше мнение о том, как и почему подход на основе метапрограммирования шаблонов занимает половину времени по сравнению с другой версией? Можно ли сделать эту программу быстрее текущей?
в основном мое намерение здесь, чтобы понять что происходит внутри, насколько это возможно.
моя машина GNU / Linux с GCC 4.8.1, и я использовал оптимизацию -o3
для обеих программ.
4 ответов
попробуйте это:
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> {};
С лязгом и -Os
, это компилируется примерно за 0,5 С и запускается в ноль времени N=40
. Ваш "обычный" подход компилируется примерно за 0,4 С и выполняется за 0,8 с. Просто для проверки результат 102334155
верно?
когда я попробовал свой собственный constexpr
решение компилятор работает в течение нескольких минут, а затем я остановил его, потому что, по-видимому, память была заполнена (компьютер начал замерзать). Компилятор пытался вычислите конечный результат, и ваша реализация крайне неэффективна для использования во время компиляции.
С помощью этого решения экземпляры шаблонов в N-2
, N-1
повторно используются при создании экземпляра N
. Так что fibonacci<40>
фактически известно во время компиляции как значение, и во время выполнения нечего делать. Это подход динамического программирования, и, конечно, вы можете сделать то же самое во время выполнения, если вы храните все значения в 0
через N-1
перед вычислением по N
.
С вашим решением, компилятор can оценить fibonacci<N>()
во время компиляции, но это не требуется. В вашем случае, все или часть вычислений остается на время выполнения. В моем случае, все попытки вычисления во время компиляции, следовательно, никогда не кончаясь.
причина в том, что ваше решение во время выполнения не является оптимальным. Для каждого номера fib функции вызываются несколько раз. Последовательность Фибоначчи имеет перекрывающиеся подзадачи, так, например,fib(6)
звонки fib(4)
и fib(5)
называет fib(4)
.
подход, основанный на шаблоне, использует (непреднамеренно) подход динамического программирования, что означает, что он хранит значения для ранее вычисленных чисел, избегая повторения. Итак, когда fib(5)
звонки fib(4)
, номер уже был рассчитано, когда fib(6)
сделал.
Я рекомендую посмотреть "динамическое программирование Фибоначчи" и попробовать это, это должно значительно ускорить процесс.
добавление -O1 (или выше) в GCC4.8.1 сделает Фибоначчи () константой времени компиляции, и весь созданный шаблон кода исчезнет из вашей сборки. Следующий код
int foo()
{
return fibonacci<40>();
}
приведет к выходу сборки
foo():
movl 2334155, %eax
ret
это дает лучшую производительность во время выполнения.
однако похоже, что вы строите без оптимизации (- O0), поэтому вы получаете что-то совсем другое. Выход сборки для каждого из 40 Фибоначчи функции выглядят в основном идентичными (за исключением случаев 0 и 1)
int fibonacci<40>():
pushq %rbp
movq %rsp, %rbp
pushq %rbx
subq , %rsp
call int fibonacci<39>()
movl %eax, %ebx
call int fibonacci<38>()
addl %ebx, %eax
addq , %rsp
popq %rbx
popq %rbp
ret
это прямо вперед, он устанавливает стек, вызывает две другие функции Фибоначчи, добавляет значение, разрывает стек и возвращает. Ни ветвей, ни сравнений.
теперь сравните это с сборкой из обычного подхода
fibonacci(int):
pushq %rbp
pushq %rbx
subq , %rsp
movl %edi, %ebx
movl , %eax
testl %edi, %edi
je .L2
movb , %al
cmpl , %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 , %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;
}
мой код основан на идее, описанной Д. кнутом в первой части его "искусство компьютерного программирования". Я не могу вспомнить точное место в этой книге, но я уверен, что алгоритм был описан там.