Вычислить число Фибоначчи (рекурсивный подход) во время компиляции (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;
}

мой код основан на идее, описанной Д. кнутом в первой части его "искусство компьютерного программирования". Я не могу вспомнить точное место в этой книге, но я уверен, что алгоритм был описан там.