Вычислить число Фибоначчи (рекурсивный подход) во время компиляции (constexpr) в С++ 11
Я написал программу вычисления числа Фибоначчи во время компиляции (constexpr)
проблема с использованием методов метапрограммирования шаблонов, поддерживаемых в С++ 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-tree-all) в g++ и обнаружил, что компилятор фактически сгенерировал 40 различных функций (например, fibonacci < 40 > , fibonacci < 39 > ... & л Фибоначи; 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 и обнаружил, что все вышеперечисленные функции
выполнялось ровно столько раз, сколько с обычным рекурсивным подходом.
Если обе версии программы выполняют функцию равное количество раз (рекурсивное), то как это достигается с помощью методов метапрограммирования шаблонов? Я также хотел бы узнать ваше мнение о том, как и почему подход, основанный на метапрограмме шаблона, занимает половину времени по сравнению с другой версией? Может ли эта программа быть выполнена быстрее, чем текущая?
В принципе, мое намерение состоит в том, чтобы понять, что происходит внутри, насколько это возможно.
Моя машина GNU/Linux с GCC 4.8.1, и я использовал оптимизацию -o3
для обеих программ.
Ответы
Ответ 1
Попробуйте следующее:
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-2
, N-1
повторно используются при создании экземпляра N
. Таким образом, fibonacci<40>
фактически известно во время компиляции как значение, и во время выполнения нечего делать. Это подход с динамическим программированием, и, конечно, вы можете сделать то же самое во время выполнения, если вы сохраните все значения в 0
через N-1
перед вычислением в N
.
С вашим решением компилятор может оценить fibonacci<N>()
во время компиляции, но не требуется. В вашем случае все или часть вычислений остается на время выполнения. В моем случае все вычисления выполняются во время компиляции, поэтому никогда не заканчиваются.
Ответ 2
Причина в том, что ваше решение во время выполнения не является оптимальным. Для каждого числа фидов функции называются несколько раз. Последовательность фибоначчи имеет перекрывающиеся подзадачи, поэтому, например, fib(6)
вызывает fib(4)
, а fib(5)
также вызывает fib(4)
.
Основанный на шаблонах подход использует (неосторожно) подход с использованием динамического программирования, что означает, что он сохраняет значения для ранее рассчитанных чисел, избегая повторения. Таким образом, когда fib(5)
вызывает fib(4)
, номер уже был рассчитан, когда fib(6)
сделал.
Я рекомендую искать "динамическое программирование фибоначчи" и пытаться это сделать, это должно резко ускорить процесс.
Ответ 3
Добавление -O1 (или выше) в GCC4.8.1 сделает fibonacci < 40 > () константой времени компиляции, и весь код, сгенерированный шаблоном, исчезнет из вашей сборки. Следующий код
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 и действовать соответствующим образом. Это сравнение не требуется в версии шаблона, потому что оно встроено в функцию через магию шаблонов. Я предполагаю, что не оптимизированная версия кода шаблона выполняется быстрее, потому что вы избегаете этих сравнений и также не должны иметь прогнозов пропущенных ветвей.
Ответ 4
Возможно, просто используйте более эффективный алгоритм?
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;
}
Мой код основан на идее, описанной Д. Кнутом в первой части его "Искусство компьютерного программирования". Я не могу вспомнить точное место в этой книге, но я уверен, что там был описан алгоритм.