简体   繁体   English

循环真的比递归快吗?

[英]Are loops really faster than recursion?

According to my professor loops are faster and more deficient than using recursion yet I came up with this c++ code that calculates the Fibonacci series using both recursion and loops and the results prove that they are very similar. 根据我的教授的说法,循环比使用递归更快,更不足,但是我想出了这个c ++代码,它使用递归和循环来计算斐波那契数列,结果证明它们非常相似。 So I maxed the possible input to see if there was a difference in performance and for some reason recursion clocked in better than using a loop. 因此,我最大化了可能的输入,以查看性能是否存在差异,并且出于某种原因,递归比使用循环更好。 Anyone know why? 有人知道为什么吗? Thanks in advanced. 提前致谢。

Here's the code: 这是代码:

#include "stdafx.h"
#include "iostream"
#include <time.h>
using namespace std;

double F[200000000];
//double F[5];

/*int Fib(int num)
{
    if (num == 0)
    {
        return 0;
    }

    if (num == 1)
    {
        return 1;
    }

    return Fib(num - 1) + Fib(num - 2);

}*/

double FiboNR(int n) // array of size n
{


    for (int i = 2; i <= n; i++)
    {
        F[i] = F[i - 1] + F[i - 2];
    }
    return (F[n]);
}

double FibMod(int i,int n) // array of size n
{
    if (i==n)
    {
        return F[i];
    }

    F[i] = F[i - 1] + F[i - 2];
    return (F[n]);
}

int _tmain(int argc, _TCHAR* argv[])
{
    /*cout << "----------------Recursion--------------"<<endl;
    for (int i = 0; i < 36; i=i+5)
    {
        clock_t tStart = clock();
        cout << Fib(i);
        printf("Time taken: %.2fs\n", (double)(clock() - tStart) / CLOCKS_PER_SEC);
        cout << " : Fib(" << i << ")" << endl;
    }*/

    cout << "----------------Linear--------------"<<endl;
    for (int i = 0; i < 200000000; i = i + 20000000)
    //for (int i = 0; i < 50; i = i + 5)
    {
        clock_t tStart = clock();
        F[0] = 0; F[1] = 1;
        cout << FiboNR(i);        
        printf("Time taken: %.2fs\n", (double)(clock() - tStart) / CLOCKS_PER_SEC);
        cout << " : Fib(" << i << ")" << endl;
    }

    cout << "----------------Recursion Modified--------------" << endl;
    for (int i = 0; i < 200000000; i = i + 20000000)
    //for (int i = 0; i < 50; i = i + 5)
    {
        clock_t tStart = clock();
        F[0] = 0; F[1] = 1;
        cout << FibMod(0,i);
        printf("Time taken: %.2fs\n", (double)(clock() - tStart) / CLOCKS_PER_SEC);
        cout << " : Fib(" << i << ")" << endl;
    }

    std::cin.ignore();
    return 0;
}

You you go by the conventional programming approach loops are faster. 您所采用的常规编程方法循环更快。 But there is category of languages called functional programming languages which does not contain loops. 但是有一类语言称为函数编程语言,其中不包含循环。 I am a big fan of functional programming and I am an avid Haskell user. 我非常喜欢函数式编程,并且是狂热的Haskell用户。 Haskell is a type of functional programming language. Haskell是一种功能编程语言。 In this instead of loops you use recursions. 在此而不是循环中,您使用递归。 To implement fast recursion there is something known as tail recursion . 为了实现快速递归,有一种称为尾递归的方法 Basically to prevent having a lot of extra info to the system stack, you write the function such a way that all the computations are stored as function parameters so that nothing needs to be stored on the stack other that the function call pointer. 基本上是为了防止向系统堆栈提供很多额外的信息,您可以这样编写函数:将所有计算都存储为函数参数,这样除函数调用指针外,不需要在堆栈上存储任何内容。 So once the final recursive call has been called, instead of unwinding the stack the program just needs to go to the first function call stack entry. 因此,一旦调用了最后的递归调用,则无需取消堆栈的堆栈,程序只需转到第一个函数调用堆栈条目即可。 Functional programming language compilers have an inbuilt design to deal with this. 函数式编程语言编译器具有内置的设计来处理此问题。 Now even non functional programming languages are implementing tail recursion. 现在,即使是非功能性编程语言也正在实现尾递归。

For example consider finding the recursive solution for finding the factorial of a positive number. 例如,考虑找到用于找到正数阶乘的递归解。 The basic implementation in C would be C语言的基本实现是

int fact(int n)
{
  if(n == 1 || n== 0)
       return 1
   return n*fact(n-1);

}

In the above approach, each time the stack is called n is stored in the stack so that it can be multiplied with the result of fact(n-1). 在上述方法中,每次将堆栈调用n时,都会将其存储在堆栈中,以便可以将它与fact(n-1)的结果相乘。 This basically happens during stack unwinding. 这基本上是在堆栈展开期间发生的。 Now check out the following implementation. 现在检查以下实现。

int fact(int n,int result)
{
   if(n == 1 || n== 0)
       return result

       return fact(n-1,n*result);

}

In this approach we are passing the computation result in the variable result. 在这种方法中,我们将计算结果传递给变量结果。 So in the end we directly get the answer in the variable result. 所以最后我们直接在变量结果中得到答案。 The only thing you have to do is that in the initial call pass a value of 1 for the result in this case. 在这种情况下,您唯一要做的就是在初始调用中为结果传递1值。 The stack can be unwound directly to its first entry. 堆栈可以直接解卷到其第一个条目。 Of course I am not sure that C or C++ allows tail recursion detection, but functional programming languages do. 当然,我不确定C或C ++是否支持尾递归检测,但是功能编程语言可以。

Your "recursion modified" version doesn't have recursion at all. 您的“已修改递归”版本完全没有递归。

In fact, the only thing enabling a non-recursive version that fills in exactly one new entry of the array is the for-loop in your main function -- so it is actually a solution using iteration also (props to immibis and BlastFurnace for noticing that). 实际上,启用非递归版本以完全填充数组的一个新条目的唯一方法是主函数中的for循环-因此,这实际上也是一种使用迭代的解决方案(immibis和BlastFurnace的属性可引起注意)那)。

But your version doesn't even do that correctly. 但是您的版本甚至无法正确执行此操作。 Rather since it is always called with i == 0 , it illegally reads F[-1] and F[-2] . 而是因为总是以i == 0调用它,所以它非法读取F[-1]F[-2] You are lucky (?) 1 the program didn't crash. 您很幸运(?) 1该程序没有崩溃。

The reason you are getting correct results is that the entire F array is prefilled by the correct version. 获得正确结果的原因是,整个F数组已由正确的版本预填充。

Your attempt to calculate Fib(2000....) isn't successful anyway, since you overflow a double . 您尝试计算Fib(2000 ....)还是失败,因为您溢出了double Did you even try running that code? 您甚至尝试运行该代码吗?

Here's a version that works correctly (to the precision of double , anyway) and doesn't use a global array (it really is iteration vs recursion and not iteration vs memoization). 这是一个可以正常工作的版本(无论如何以double的精度表示)并且不使用全局数组(实际上是迭代与递归,而不是迭代与记忆)。

#include <cstdio>
#include <ctime>
#include <utility>


double FiboIterative(int n)
{
    double a = 0.0, b = 1.0;

    if (n <= 0) return a;

    for (int i = 2; i <= n; i++)
    {
        b += a;
        a = b - a;
    }
    return b;
}

std::pair<double,double> FiboRecursive(int n)
{
    if (n <= 0) return {};

    if (n == 1) return {0, 1};

    auto rec = FiboRecursive(n-1);

    return {rec.second, rec.first + rec.second};
}

int main(void)
{
    const int repetitions = 1000000;
    const int n = 100;
    volatile double result;

    std::puts("----------------Iterative--------------");
    std::clock_t tStart = std::clock();
    for( int i = 0; i < repetitions; ++i )
        result = FiboIterative(n);
    std::printf("[%d] = %f\n", n, result);
    std::printf("Time taken: %.2f us\n", (std::clock() - tStart) / 1.0 / CLOCKS_PER_SEC);

    std::puts("----------------Recursive--------------");
    tStart = std::clock();
    for( int i = 0; i < repetitions; ++i )
        result = FiboRecursive(n).second;
    std::printf("[%d] = %f\n", n, result);
    std::printf("Time taken: %.2f us\n", (std::clock() - tStart) / 1.0 / CLOCKS_PER_SEC);
    return 0;
}

-- -

1 Arguably anything that hides a bug is actually unlucky. 1可以说,任何隐藏错误的东西实际上都是不幸的。

I don't think this is not a good question. 我认为这不是一个好问题。 But maybe the answer why is somehow interesting. 但也许答案为什么有些有趣。

At first let me say that generally the statement is probably true. 首先,让我说,一般而言,该说法可能是正确的。 But ... 但是...

Questions about performance of c++ programs are very localized. 有关c ++程序性能的问题非常本地化。 It's never possible to give a good general answer. 永远不可能给出一个很好的一般答案。 Every example should be profiled an analyzed separately. 每个示例都应单独分析。 It involves lots of technicalities. 它涉及许多技术。 c++ compilers are allowed to modify program practically as they wish as long as they don't produce visible side effects (whatever precisely that means). 只要c ++编译器不产生明显的副作用(无论这意味着什么),就可以按照他们的意愿实际修改程序。 So as long as your computation gives the same result is fine. 因此,只要您的计算得出相同的结果就可以了。 This technically allows to transform one version of your program into an equivalent even from recursive version into the loop based and vice versa. 从技术上讲,这允许将程序的一个版本转换为等效版本,甚至从递归版本转换为基于循环的版本,反之亦然。 So it depends on compiler optimizations and compiler effort. 因此,这取决于编译器优化和编译器的工作量。

Also, to compare one version to another you would need to prove that the versions you compare are actually equivalent. 另外,要将一个版本与另一个版本进行比较,您需要证明所比较的版本实际上是等效的。

It might also happen that somehow a recursive implementation of algorithm is faster than a loop based one if it's easier to optimize for the compiler. 如果更容易针对编译器进行优化,那么递归算法的实现可能比基于循环的算法更快。 Usually iterative versions are more complex, and generally the simpler the code is, the easier it is for the compiler to optimize because it can make assumptions about invariants, etc. 通常,迭代版本会更复杂,并且代码通常越简单,编译器就越容易优化,因为它可以对不变量进行假设,等等。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM