我测量运行时间的方法有缺陷吗?

时间:2010-10-22 23:17:59

标签: c# benchmarking

8 个答案:

答案 0 :(得分:8)

我首先想到的是一个简单的循环

for (int i = 0; i < x; i++)
{
    timer.Start();
    test();
    timer.Stop();
}

与以下相比有点愚蠢:

timer.Start();
for (int i = 0; i < x; i++)
    test();
timer.Stop();

原因是(1)这种“for”循环具有非常小的开销,如此小,以至于即使test()只需要一微秒也几乎不值得担心,以及(2)timer.Start( )和timer.Stop()有自己的开销,这可能比for循环更多地影响结果。也就是说,我看了一下Reflector中的秒表并注意到Start()和Stop()相当便宜(考虑到所涉及的数学,调用Elapsed *属性可能更贵)。

确保秒表的IsHighResolution属性为true。如果它是假的,秒表使用DateTime.UtcNow,我相信它只会每15-16毫秒更新一次。

<强> 1。获得每次迭代的运行时间通常都是一件好事吗?

通常不需要测量每个单独迭代的运行时间,但 有助于了解不同迭代之间性能的差异。为此,您可以计算最小值/最大值(或k个异常值)和标准差。只有“中位数”统计信息要求您记录每次迭代。

如果您发现标准差很大,那么您可能有理由记录每次迭代,以便探究时间不断变化的原因。

有些人编写了小框架来帮助您进行性能基准测试。例如,CodeTimers。如果您正在测试的东西非常小而且基本库的开销很重要,请考虑在基准库调用的lambda内的for循环中运行该操作。如果操作非常小以致于循环的开销很重要(例如测量乘法的速度),则使用手动循环展开。但是如果您使用循环展开,请记住大多数真实世界的应用程序不使用手动循环展开,因此您的基准测试结果可能会夸大实际性能。

对于我自己,我写了一个用于收集最小值,最大值,平均值和标准差的小类,可以用于基准测试或其他统计:

// A lightweight class to help you compute the minimum, maximum, average
// and standard deviation of a set of values. Call Clear(), then Add(each
// value); you can compute the average and standard deviation at any time by 
// calling Avg() and StdDeviation().
class Statistic
{
    public double Min;
    public double Max;
    public double Count;
    public double SumTotal;
    public double SumOfSquares;

    public void Clear()
    {
        SumOfSquares = Min = Max = Count = SumTotal = 0;
    }
    public void Add(double nextValue)
    {
        Debug.Assert(!double.IsNaN(nextValue));
        if (Count > 0)
        {
            if (Min > nextValue)
                Min = nextValue;
            if (Max < nextValue)
                Max = nextValue;
            SumTotal += nextValue;
            SumOfSquares += nextValue * nextValue;
            Count++;
        }
        else
        {
            Min = Max = SumTotal = nextValue;
            SumOfSquares = nextValue * nextValue;
            Count = 1;
        }
    }
    public double Avg()
    {
        return SumTotal / Count;
    }
    public double Variance()
    {
        return (SumOfSquares * Count - SumTotal * SumTotal) / (Count * (Count - 1));
    }
    public double StdDeviation()
    {
        return Math.Sqrt(Variance());
    }
    public Statistic Clone()
    {
        return (Statistic)MemberwiseClone();
    }
};

<强> 2。在实际计时开始之前还有一小圈的运行吗?

您测量的哪些迭代取决于您是否最关心启动时间,稳态时间或总运行时间。通常,在“启动”运行时分别记录一个或多个运行可能很有用。您可以预期第一次迭代(有时不止一次)运行得更慢。作为一个极端的例子,我的GoInterfaces库一直需要大约140毫秒来产生它的第一个输出,然后它在大约15毫秒内再做9个。

根据基准测量的内容,您可能会发现如果在重新启动后立即运行基准测试,则第一次迭代(或前几次迭代)将非常缓慢地运行。然后,如果第二次运行基准测试,第一次迭代会更快。

第3。循环中强制Thread.Yield()是否会帮助或损害CPU绑定测试用例的时间?

我不确定。它可以清除处理器缓存(L1,L2,TLB),这不仅会降低整体基准速度,还会降低测量速度。你的结果将更加“人为”,而不是反映你在现实世界中会得到什么。也许更好的方法是避免在基准测试的同时运行其他任务。

答案 1 :(得分:4)

无论用于计时功能的机制(这里的答案似乎都很好),有一个非常简单的技巧可以消除基准测试代码本身的开销,即循环开销,计时器读数和方法 - 拨打:

首先使用空Func<T>调用您的基准测试代码,即

void EmptyFunc<T>() {}

这将为您提供时间开销的基线,您可以从实际基准函数的后一个测量中基本减去。

“基本上”我的意思是,由于垃圾收集和线程以及进程调度,在计时某些代码时始终存在变化的空间。一种务实的方法将是例如要对空函数进行基准测试,找出平均开销(总时间除以迭代次数),然后从实际基准函数的每个时序结果中减去该数字,但不要让它低于0,这是没有意义的。

当然,您必须稍微重新安排基准测试代码。理想情况下,您需要使用完全相同的代码来对空函数和真实基准函数进行基准测试,因此我建议您将时序循环移动到另一个函数或至少保留两个循环完全相似。总结

  1. 对空函数进行基准测试
  2. 计算结果的平均开销
  3. 对真实的测试功能进行基准测试
  4. 从这些测试结果中减去平均开销
  5. 你已经完成了
  6. 通过这样做,实际的计时机制突然变得不那么重要了。

答案 2 :(得分:2)

我认为您的第一个代码示例似乎是最好的方法。

您的第一个代码示例很小,干净且简单,并且在测试循环期间不会使用任何主要的抽象,这可能会引入隐藏的开销。

使用秒表类是一件好事,因为它简化了通常必须编写的代码以获得高分辨率的时序。

您可能会考虑的一件事是提供选项,在进入定时循环之前,将测试迭代次数较少,以便预热任何缓存,缓冲区,连接,句柄,套接字,线程池线程等测试例程可以运动。

HTH。

答案 3 :(得分:1)

我倾向于同意@ Sam Saffron关于使用一个秒表而不是每次迭代一次。在您的示例中,默认情况下执行1000000次迭代。我不知道创建单个秒表的成本是多少,但是你创造了1000000个。可以想象,这本身就会影响您的测试结果。我重新设计了你的“最终实现”,以便在不创建1000000秒表的情况下测量每次迭代。当然,因为我正在保存每次迭代的结果,所以我分配1000000个长,但乍一看似乎总体影响要小于分配那么多的秒表。我没有将我的版本与你的版本进行比较,看看我的版本是否会产生不同的结果。

static void Test2<T>(string testName, Func<T> test, int iterations = 1000000)
{
  long [] results = new long [iterations];

  // print header 
  for (int i = 0; i < 100; i++) // warm up the cache 
  {
    test();
  }

  var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process 

  long start;

  for (int i = 0; i < results.Length; i++)
  {
    start = Stopwatch.GetTimestamp();
    test();
    results[i] = Stopwatch.GetTimestamp() - start;
  }

  timer.Stop();

  double ticksPerMillisecond = Stopwatch.Frequency / 1000.0;

  Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t / ticksPerMillisecond), results.Average(t => t / ticksPerMillisecond), results.Max(t => t / ticksPerMillisecond), results.Sum(t => t / ticksPerMillisecond));
  Console.WriteLine("Ticks:    {0,3}/{1,10}/{2,8} ({3,10})", results.Min(), results.Average(), results.Max(), results.Sum());

  Console.WriteLine();
}

我在每次迭代中使用秒表的静态GetTimestamp方法两次。两者之间的差值将是迭代中花费的时间量。使用Stopwatch.Frequency,我们可以将delta值转换为毫秒。

使用Timestamp和Frequency来计算性能并不一定要像直接使用Stopwatch实例一样清晰。但是,为每次迭代使用不同的秒表可能不如使用单个秒表来测量整个事物那样清晰。

我不知道我的想法比你的想法更好或更差,但它略有不同; - )

我也同意热身循环。根据您的测试工作,可能会有一些固定的启动成本,您不希望影响整体结果。启动循环应该消除它。

由于保存整个数值(或定时器)所需的存储成本,保持每个单独的定时结果可能会适得其反。为了减少内存,但需要更多的处理时间,您可以简单地对增量求和,计算最小值和最大值。这有可能会丢掉你的结果,但是如果你主要关注基于invidivual迭代测量生成的统计数据,那么你可以在时间增量检查之外进行最小和最大计算:

static void Test2<T>(string testName, Func<T> test, int iterations = 1000000)
{
  //long [] results = new long [iterations];
  long min = long.MaxValue;
  long max = long.MinValue;

  // print header 
  for (int i = 0; i < 100; i++) // warm up the cache 
  {
    test();
  }

  var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process 

  long start;
  long delta;
  long sum = 0;

  for (int i = 0; i < iterations; i++)
  {
    start = Stopwatch.GetTimestamp();
    test();
    delta = Stopwatch.GetTimestamp() - start;
    if (delta < min) min = delta;
    if (delta > max) max = delta;
    sum += delta;
  }

  timer.Stop();

  double ticksPerMillisecond = Stopwatch.Frequency / 1000.0;

  Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", min / ticksPerMillisecond, sum / ticksPerMillisecond / iterations, max / ticksPerMillisecond, sum);
  Console.WriteLine("Ticks:    {0,3}/{1,10}/{2,8} ({3,10})", min, sum / iterations, max, sum);

  Console.WriteLine();
}

看起来很老的学校没有Linq的操作,但它仍然可以完成工作。

答案 4 :(得分:0)

方法2中的逻辑对我来说感觉“更严厉”,但我只是一名CS学生。

我遇到了您可能感兴趣的链接: http://www.yoda.arachsys.com/csharp/benchmark.html

答案 5 :(得分:0)

根据您正在测试的代码的运行时间,测量单个运行非常困难。如果您的测试代码的运行时间是多秒,那么您对特定运行进行计时的方法很可能不会成为问题。如果它在毫秒附近,你的结果可能会非常多。如果你是在错误的时刻有一个上下文切换或从交换文件读取,该运行的运行时将与平均运行时不成比例。

答案 6 :(得分:0)

我有类似的question here

我更喜欢使用单一秒表的概念,特别是如果你是微型benchamrking。您的代码不考虑可能影响性能的GC。

我认为强制GC集合在运行测试运行之前非常重要,我也不确定100次预热运行的重点是什么。

答案 7 :(得分:0)

我会倾向于最后一个,但我会考虑启动和停止计时器的开销是否大于循环本身的开销。

要考虑的一件事是,CPU缓存未命中的影响是否实际上是一个公平的尝试反击?

利用CPU缓存是一种方法可能会击败另一种方法,但在实际情况下,每次调用都可能存在缓存缺失,因此这种优势变得无关紧要。在这种情况下,不太好用缓存的方法可能会成为具有更好的实际性能的方法。

基于数组或单链接列表的队列就是一个例子;当缓存行在调用之间没有重新填充时,前者几乎总是具有更高的性能,但是调整大小操作比后者更多。因此,后者可以在实际案例中获胜(尤其是因为它们更容易以无锁形式编写),即使它们在快速迭代的时序测试中几乎总是会丢失。

由于这个原因,还可以尝试一些迭代来实际强制刷新缓存。想不出现在最好的办法是什么,所以如果我这样做的话,我可能会回来加上这个。