【问题标题】:Inconsistencies in recursive vs iterative speed递归与迭代速度的不一致
【发布时间】:2014-10-07 15:12:14
【问题描述】:

我做了一个程序来比较迭代速度和递归速度,操作非常简单。 我注意到一些我无法解释的奇怪现象。 根据处理的值的数量,迭代或递归的速度更快。但这并不一致。

  • 大约 10 次,迭代速度更快。
  • 大约 100 次,迭代速度更快。
  • 对于大约 200,递归更快。
  • 对于大约 1000,递归更快。
  • 对于大约 5000,迭代更快。
  • 对于大约 10000,迭代更快。
  • 除此之外,递归运行到堆栈溢出,所以不能测试更高。

为什么迭代过程对于较低和较高的可测试量来说更快,但对于介于两者之间的量则不然?

我对此缺乏更深入的了解,如果有人可以向我解释这一点,我将不胜感激。

这里是那些想要自己测试的人的代码。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Compare_Recursive_Iterative
{
    class Program
    {
        static void Main(string[] args)
        {
            // Amount of values processed.
            long testNumber = 200;
            // Variables to measure time taken.
            double timeIterative, timeRecursive, timeTotal;

            // Iterative process + time elapsed calculation
            DateTime start = DateTime.Now;
            long retVal = 0;
            for(long i = 1; i <= testNumber; i++)
            {
                retVal = 0;
                for (long x = 1; x <= i; x++)
                { 
                    retVal += x;
                }
                Console.WriteLine("For " + i + ": " + retVal);
            }
            TimeSpan timeDiff = DateTime.Now - start;
            timeIterative = timeDiff.TotalMilliseconds;

            // Recursive process + time elapsed calculation
            DateTime start2 = DateTime.Now;
            for (long i = 1; i <= testNumber; i++)
            {
                Console.WriteLine("For " + i + ": " + calculate(i));
            }
            timeDiff = DateTime.Now - start2;
            timeRecursive = timeDiff.TotalMilliseconds;

            // Results to console.
            Console.WriteLine("Iterative: " + timeIterative + "ms /// Recursive: " + timeRecursive + "ms");
            timeDiff = DateTime.Now - start;
            timeTotal = timeDiff.TotalMilliseconds;
            Console.WriteLine("Total time needed: " + timeTotal + "ms");
            Console.WriteLine("Iterative - Recursive: " + (timeIterative - timeRecursive) + "ms");
            Console.Read();
        }


        // Recursive method
        static long calculate(long x)
        {
            if (x <= 1)
                return x;
            else
                return x + calculate(x - 1);
        }

    }
}

【问题讨论】:

  • 老实说这里的计算太少了,DateTime.Now 不是用于高精度测量的,结果主要是由于写入控制台需要多长时间。
  • 虽然递归实现通常需要相对昂贵的调用指令,但选择递归或迭代的最显着原因是基于深度(由于潜在的堆栈溢出)和不是性能本身。
  • 另请阅读Eric Lippert's series on writing benchmarks。编写它们时有一些常见的陷阱需要注意,在这个示例中可以找到一些。

标签: c# performance recursion iteration


【解决方案1】:

大部分时间将花在您的示例中的 Console.WriteLine 上,这比添加一些工作要做的工作要多得多,因此如果您想进行测量,那么您的测量毫无意义

1) 确保您只测量您想要测量的内容(不要写入任何输出,您在测试后执行此操作)

2) 至少使用秒表,DateTime.Now 一点也不精确

3) 将两个测试隔离在它们自己的函数中,并在测试前分别调用它们一次,以确保您没有测量预热时间。

4) 大量调用这两个函数(可能是 1000 次?),然后记下每个函数的最小/最大/平均时间

5) 确保您没有在系统上执行任何其他操作,并且没有运行任何密集的操作(在运行时禁用防病毒等)。

6) 同样非常非常重要,不要在调试模式下进行测试,先切换到发布!!!调试模式在某些情况下会大大减慢速度,而在其他情况下几乎不会,这意味着函数 A 在调试时可能比 B 快得多,而在发布时则相反。也不要使用附加的调试器进行测试,只需从文件夹中启动 exe。

因为您没有执行第 1 步,这就是导致您的问题的原因,因为所有时间都花得很好,几乎在您自己的代码之外,其他点都在那里,所以您可以考虑所有这些而不是在这里来回问答。

制作了一个示例来向您展示,可能并不完美,但更接近基准应该是什么样子,还请注意,如果您确实想比较内联测试,您可能希望将测试放回主代码中(与递归相比必须是函数)

void Main()
{
var TargetNumber = 2000;
var TestRuns = 10;
//warmup both methods
calculate(100);
TestIterative(100);

Stopwatch sw = new Stopwatch();

var RecursiveTimes = new List<long>();

for(int run = 1;run<=TestRuns;run++)
{
    sw.Restart();
    for (int i = 1; i <= TargetNumber; i++)
    {
        calculate(i);
    }
    sw.Stop();
    RecursiveTimes.Add(sw.ElapsedMilliseconds);
}

var IterativeTimes = new List<long>();

for(int run = 1;run<=TestRuns;run++)
{
    sw.Restart();
    for (int  i = 1; i <= TargetNumber; i++)
    {
        TestIterative(i);
    }
    sw.Stop();
    IterativeTimes.Add(sw.ElapsedMilliseconds);
}

Console.WriteLine("Iterative : " + IterativeTimes.Average() + " ms on average. Min and max : " + IterativeTimes.Min() + " / " + IterativeTimes.Max());
Console.WriteLine("Recursive : " + RecursiveTimes.Average() + " ms on average. Min and max : " + RecursiveTimes.Min() + " / " + RecursiveTimes.Max());  
}

static long TestIterative(long x)
{
    long retVal = 0;
    for (long y = 1; y <= x; y++)
    { 
        retVal += y;
    }
    return retVal;
}

static long calculate(long x)
{
    if (x <= 1)
        return x;
    else
        return x + calculate(x - 1);
}

还请注意,我将您要测试的数字(从 0 到 TargetNumber)与测试运行的数量分开,因为它们没有理由链接(TestRuns),这样您就可以多次测试一个小集合或一个大集合几次。

正如您所看到的,所有输出都移到程序的末尾,因为它很昂贵而且您不想测量它,即使将时间添加到列表中也是在计时区域之外完成的,我正在计时每个测试(不在测试中),因为时间太小,我们最终会得到很多零。

另外,一个好的基准测试是用可用数据测试实际可用代码,不要尝试对什么都不做的“微基准测试”代码,你不能真正对在硬件上做一些添加所花费的时间进行基准测试每秒执行十亿次。

还有一个很好的经验法则,除非它使您的代码变得更加复杂,否则请始终使用迭代,而不是为了性能(但它确实更好),但主要是因为如果您没有理由让自己的堆栈溢出可以避免。

【讨论】:

  • 我真的不知道我在测试中实际上搞砸了多少。我想我还有很长的路要走,还有很多东西要学,你所有的建议都是一个好的开始!现在,开始进行更适当的测试!非常感谢!
  • 在评论中添加它,因为它不是答案的一部分,但是,为什么你在那里使用 long 而不是 int?它可能会减慢 32 位的速度,而您什么也得不到(因为在您进入使用 int64 所需的数字之前,递归方法会以 stackoverflow 方式崩溃)
  • 添加了一个例子,希望对您有所帮助。
  • 再次感谢您提供此示例!我还没来得及做这件事。自从我开始使用它以来,我一直使用 long 而不是 int,然后才意识到 stackoverflow 会发生多快。你给了很多很好的建议,我似乎错过了一些非常明显的东西。你帮了大忙。现在我在对实际代码进行基准测试时有了一些参考,这可能很快就会派上用场!您让我在 stackoverflow 上的第一个问题成为了一次很棒的体验,谢谢! :)
猜你喜欢
  • 2012-04-26
  • 2023-03-08
  • 1970-01-01
  • 2018-08-16
  • 2012-08-29
  • 2021-05-26
  • 1970-01-01
  • 2021-11-09
  • 2012-09-15
相关资源
最近更新 更多