2015-03-16 2 views
2

Есть ли заметная разница во времени вычислений между фибоначчими типа Фибоначчи в стиле рекурсии и фибоначчи в стиле петли? Я продолжаю работать с Фибоначчи до 40 мест, используя рекурсию, а затем используя цикл сразу после этого. Кажется, что разница во времени вычисления составляет только академический.Время вычисления Фибоначчи

Написано в C

рекурсивной решение:

int main(int argc, const char * argv[]) { 
    int n, i = 0, c; 
    printf("Please enter an integer:\n"); 
    scanf("%d", &n); 
    for (c = 1 ; c <= n ; c++) 
    { 
     printf("%lu ", fibonacci(i)); 
     i++; 
    } 
    return 0; 
    } 

long fibonacci(long n) 
{ 
    if (n == 0) 
     return 0; 
    else if (n == 1) 
     return 1; 
    else 
     return (fibonacci(n-1) + fibonacci(n-2)); 
}; 

для цикла решения:

int main(int argc, const char * argv[]) { 
int n, first = 0, second = 1, next, c; 
    printf("Please enter an integer:\n"); 
    scanf("%d", &n); 
    for (c = 0 ; c < n ; c++) 
    { 
     if (c <= 1) 
      next = c; 
     else 
     { 
      next = first + second; 
      first = second; 
      second = next; 
     } 
     printf("%d ",next); 
    } 
    return 0; 
}; 
+0

Похоже, вы уже ответили на свой вопрос в первом абзаце. –

+1

Время выполнения для одного вычисления не будет значительным. Если вы хотите сравнить, вам нужно вычислить много (10^7?) Раз. – mattm

+0

Интересно. В последнее время я очень заинтересовался последовательностью Фибоначчи. Как его обнаружено в столь многих представлениях в природе красиво и как эта последовательность используется так часто в бенчмаркингах. –

ответ

4

Обычный метод рекурсии чрезвычайно медленный по сравнению с хвостовыми рекурсивными и итеративными версиями. В приведенном ниже примере для версии итерации используется развернутый цикл вместе с Duff's Device для ввода цикла. Для 32-битных целых чисел без знака предел равен fib (47), для 64-битных целых чисел без знака предел равен fib (93).

Сроки были выполнены с Intel 2600K 3.4ghz, XP X64, 64-разрядным режимом. Частота счетчика XP или XP X64 - это то же самое, что и часы процессора, 3.4ghz, но накладные расходы операционной системы (например, прерывания) влияют на время, если продолжительность мала.

тайминги для FIB (40):

fibr() # of microseconds 485468.8 
fibt() # of microseconds   0.2 
fibi() # of microseconds   0.2 

синхронизации в течение 94 циклов, п = 0 до 93:

fibt() # of microseconds   7 
fibi() # of microseconds   5 

Пример кода:

typedef unsigned long long UI64; 

UI64 fibr(UI64 n) 
{ 
    if(n < 2) 
     return n; 
    return fibr(n-1) + fibr(n-2); 
} 

// call with fibt(n, 0, 1) 
UI64 fibt(UI64 n, UI64 res, UI64 next) 
{ 
    if (n == 0) 
     return res; 
    return fibt(n - 1, next, res + next); 
} 

UI64 fibi(UI64 n) 
{ 
UI64 f0, f1, i; 
    if(n < 2) 
     return n; 
    n -= 2; 
    f1 = f0 = 1; 
    i = 0; 
    switch(n%8){ 
     do{ 
      f1 += f0; 
      case 7: 
      f0 += f1; 
      case 6: 
      f1 += f0; 
      case 5: 
      f0 += f1; 
      case 4: 
      f1 += f0; 
      case 3: 
      f0 += f1; 
      case 2: 
      f1 += f0; 
      case 1: 
      f0 += f1; 
      case 0: 
      continue; 
     }while(n >= (i += 8)); 
    } 
    return f0; 
} 

Альтернативный вариант Фиби(), без проверки n < 2. Что f0 и f1 представляют изменения в цикле, предназначенные для конечной суммы в f0, поэтому начальное состояние того, что f0 и f1 представляют, зависит от того, является ли n четным или нечетным. Если n четно, f0 = fib (0) = 0, f1 = fib (-1) = 1, если n нечетно, f1 = fib (0) = 0, f0 = fib (-1) = 1. (Если вам интересно, fib (-1) = 1, fib (-2) = -1, fib (-3) = 2, fib (-4) = -3, fib (-5) = 5, fib (-6) = -8, ...).

Для объяснения логики здесь для n четного случая fib (-1) = f1 = 1, fib (0) = f0 = 0, то fib (1) = (f1 + = f0), fib (2) = (f0 + = f1), fib (3) = (f1 + = f0), fib (4) = (f0 + = f1), ....

UI64 fibi(UI64 n) 
{ 
UI64 f0, f1, i; 
    f0 = n & 1;   // if n even, f0=0, f1=1 
    f1 = 1 - f0;  // else  f1=0, f0=1 
    i = 0; 
    switch(n%8){ 
     do{ 
      f1 += f0; 
      case 7: 
      f0 += f1; 
      case 6: 
      f1 += f0; 
      case 5: 
      f0 += f1; 
      case 4: 
      f1 += f0; 
      case 3: 
      f0 += f1; 
      case 2: 
      f1 += f0; 
      case 1: 
      f0 += f1; 
      case 0: 
      continue; 
     }while(n >= (i += 8)); 
    } 
    return f0; 
} 
+0

Это окончательное доказательство. Большое спасибо за то, что вы испытали эту удивительную и красивую последовательность! Эти результаты чрезвычайно интересны. Я никогда не видел оператора switch, используемого для этого, но он агрессивно эффективен. Я просто люблю эту последовательность. –

+1

@FrankPalmisano. Другим трюком здесь является только две переменные вместо трех, которые используются в развернутом цикле. Чувство того, что f0 и f1 представляют собой изменения в развернутом цикле, заканчивается правильным значением в f0. – rcgldr

+0

Я продолжаю отлаживать, чтобы увидеть это. Если бы мы могли найти способ ограничить количество регистров, используемых также для оператора switch. Однако лучше использовать регистры и иметь его в стеке; с рекурсивной функцией. –

1

для цикла решения быстрее. Причины:

  1. нет вызовов функций (при условии, что вызовы функций являются дорогостоящими)
  2. вычисления n-е использует n дополнения (итерации цикла n раза), в то время как рекурсивное решение использует добавление каждого вызова функции, которая подводит до O(1.6n) звонки, следовательно O(1.6n) добавление. Стоимость исходила от двойных рекурсивных вызовов - когда рекурсивная функция запрашивала n-й элемент, ей нужно было снова вычислить n-1-й и n-2-й элементы с самого начала, но не помню их.
+0

Математически ваше описание имеет смысл. Ясно, что рекурсия медленнее, чем цикл. Однако, когда речь идет о компиляции, как в другом ответе, дальнейшая оптимизация, похоже, была сделана. –

+1

Правда. Но главная проблема заключается в том, чтобы понять, что существует большая разница во временной сложности между методами, причина, по которой они принимают почти одно и то же время, на самом деле потому, что компилятор делает эти случаи почти одинаковыми. – avim

+0

Это не 2^n звонки. Это около 1.608^n звонков. – gnasher729

2

Для петли это не обязательно быстрее. В обычных языках, таких как Java, C и Python, рекурсия довольно дорога по сравнению с итерацией, потому что для этого требуется выделение нового фрейма стека.

В C/C++ можно исключить возможность оптимизации компилятора для выполнения хвостовой рекурсии, которая преобразует определенные типы рекурсии (фактически, определенные типы хвостовых вызовов) в переходы вместо вызовов функций. Чтобы компилятор выполнял эту оптимизацию, необходимо, чтобы последнее, что функция делает перед тем, как она вернется, вызывает другую функцию (в этом случае сама).

Примером функции Фибоначчи может быть, как это:

int fib_tail(int n, int res, int next) 
{ 
    if (n == 0) { 
    return res; 
    } 
    return fib_tail(n - 1, next, res + next); 
} 

и на уровне сборки, что позволяет оптимизировать компилятор, он будет реализован в виде петли, например разделяя один и тот же стек кадров между вызовами.

Я недавно написал об этом article.

Надеюсь, это поможет.

+0

Благодарим вас за разъяснения. Его приятно знать, что компилятор выполнит всю оптимизацию и вызовет еще меньшие различия во времени. Однако математически, другой ответ также является правильным. Ваш ответ более важен в реальных сценариях, где он действительно имеет значение. –

+1

добро пожаловать. Будьте очень осторожны.Рекурсия хвоста не гарантируется компилятором C/C++, поэтому вам всегда нужно проверить, включено ли это или нет (например, посмотреть сборку). Худшая проблема, которая может возникнуть у вас, заключается в получении исключения stackoverflow при использовании рекурсивной функции, которая по какой-то причине не была оптимизирована. – codingadventures

+0

Какой компилятор C или C++ вы используете, который выполняет функцию возврата хвоста для функции фибоначчи? Просто любопытно. Я совершенно уверен, что Кланг этого не делает. – gnasher729

0

Как вы измерили разницу в скорости?

Наивная рекурсивная реализация функции Фибоначчи занимает около 100 миллионов вызовов функций для вычисления f (40). На современном компьютере, который будет достаточно быстрым, чтобы вы не могли на это смотреть с секундомером.

Расчет f (50) занимает около 10 миллиардов вызовов функций, что будет заметной задержкой. f (60) принимает триллион вызовов функций или около часа. f (70) принимает около 200 триллионов вызовов функций или несколько дней. f (80) принимает около 20 квадриллионов вызовов функций или около года.

Я бы не назвал эту разницу академической.

+0

У вас есть хороший момент, и я не думал об этом до недавнего времени. Вы правы и представляете действительный аргумент. –