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

对不起,这是一个很长的问题,但我只是在分析这个问题时解释我的思路。 最后的问题。

我已经了解了测量代码运行时间的方法。 它运行多次以获得平均运行时间来计算每次运行的差异,并获得更好地利用缓存的时间。

为了测量某人的运行时间,我在多次修改后想出了这段代码。

最后,我最终得到了这个代码,它产生了我打算捕获的结果,而没有给出误导性的数字:

// implementation C static void Test(string testName, Func test, int iterations = 1000000) { Console.WriteLine(testName); Console.WriteLine("Iterations: {0}", iterations); var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList(); var timer = System.Diagnostics.Stopwatch.StartNew(); for (int i = 0; i  t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), timer.ElapsedMilliseconds); Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), timer.ElapsedTicks); Console.WriteLine(); } 

在我看到的测量运行时间的所有代码中,它们通常采用以下forms:

 //接近1个伪代码
启动计时器;
循环N次:
    运行测试代码(直接或通过function);
停止计时器;
报告结果;

这在我的脑海里很好,因为数字,我有总的运行时间,可以很容易地计算平均运行时间,并具有良好的缓存局部性。

但是我认为重要的一组值是最小和最大迭代运行时间。 无法使用上述表格计算。 因此,当我编写测试代码时,我以这种forms编写它们:

 //接近2个伪代码
循环N次:
    启动计时器;
    运行测试代码(直接或通过function);
    停止计时器;
    商店结果;
报告结果;

这很好,因为我可以找到最小,最大和平均时间,我感兴趣的数字。直到现在我才意识到这可能会导致结果偏差,因为缓存可能会受到影响,因为循环不是很紧给我不太理想的结果。


我编写测试代码的方式(使用LINQ)增加了额外的开销,我知道但忽略了,因为我只是测量运行代码,而不是开销。 这是我的第一个版本:

 // implementation A static void Test(string testName, Func test, int iterations = 1000000) { Console.WriteLine(testName); var results = Enumerable.Repeat(0, iterations).Select(i => { var timer = System.Diagnostics.Stopwatch.StartNew(); test(); timer.Stop(); return timer; }).ToList(); Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds)); Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks)); Console.WriteLine(); } 

在这里我认为这很好,因为我只测量运行测试function所花费的时间。 与LINQ相关的开销不包括在运行时间中。 为了减少在循环中创建计时器对象的开销,我进行了修改。

 // implementation B static void Test(string testName, Func test, int iterations = 1000000) { Console.WriteLine(testName); Console.WriteLine("Iterations: {0}", iterations); var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList(); results.ForEach(t => { t.Start(); test(); t.Stop(); }); Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), results.Sum(t => t.ElapsedMilliseconds)); Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), results.Sum(t => t.ElapsedTicks)); Console.WriteLine(); } 

这改善了整体时间,但造成了一个小问题。 我通过添加每次迭代的次数在报告中添加了总运行时间,但由于时间很短并且没有反映实际运行时间(通常更长),因此给出了误导性数字。 我现在需要测量整个循环的时间,所以我离开了LINQ,最后得到了我现在在顶部的代码。 这种混合动力在我认为最重要的时候是最小的开销AFAIK。 (启动和停止计时器只是查询高分辨率计时器)同样,任何上下文切换对我来说都不重要,因为它无论如何都是正常执行的一部分。

有一次,我强迫线程在循环内产生,以确保在方便的时间某个时刻给它机会(如果测试代码是CPU绑定的并且根本不阻塞)。 我并不太关心正在运行的进程,这可能会使缓存变得更糟,因为无论如何我都会单独运行这些测试。 但是,我得出结论,对于这个特殊情况,没有必要。 虽然如果它在一般情况下certificate是有益的,我可能会将它纳入最终的最终版本。 也许作为某些代码的替代算法。


现在我的问题:

  • 我做出了一些正确的选择吗? 一些错误的?
  • 我是否对思考过程中的目标做出了错误的假设?
  • 最小或最大运行时间是真的有用的信息还是失败的原因?
  • 如果是这样,一般来说哪种方法会更好? 循环中运行的时间(方法1)? 或者只运行相关代码的时间(方法2)?
  • 我的混合方法一般可以使用吗?
  • 应该屈服(出于上一段中解释的原因)还是对时间的伤害超过必要的?
  • 有没有更优先的方式来做到这一点,我没有提到?

为了清楚起见,我不是在寻找一个通用的,随处可用的精确定时器。 我只想知道一个算法,当我想要快速实现时,我应该使用这个算法,合理准确的计时器来衡量当库或其他第三方工具不可用时的代码。

如果没有异议,我倾向于以这种forms编写我的所有测试代码:

 // final implementation static void Test(string testName, Func test, int iterations = 1000000) { // print header var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList(); for (int i = 0; i < 100; i++) // warm up the cache { test(); } var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process for (int i = 0; i < results.Count; i++) { results[i].Start(); // time individual process test(); results[i].Stop(); } timer.Stop(); // report results } 

对于赏金,我希望能够回答上述所有问题。 我希望得到一个很好的解释,我的想法是否能够很好地certificate这里的代码(并且可能是关于如何在次优的情况下改进它的想法),或者如果我错了一点,解释为什么它是错误的和/或不必要的,如果适用,提供更好的选择。

总结重要问题和我对决策的看法:

  1. 获得每个迭代的运行时间通常是一件好事吗?
    通过每次迭代的时间,我可以计算其他统计信息,如最小和最大运行时间以及标准偏差。 所以我可以看看是否存在缓存或其他未知因素可能会导致结果偏差。 这导致了我的“混合”版本。
  2. 在实际计时开始之前还有一小圈的运行吗?
    根据我对Sam Saffron关于循环的想法的回应,这是为了增加缓存不断访问的内存的可能性。 这样我只测量所有内容被缓存的时间,而不是某些未缓存内存访问的情况。
  3. 循环中强制Thread.Yield()是否会帮助或损害CPU绑定测试用例的时间?
    如果进程受CPU限制,则由于CPU上的时间不足,OS调度程序会降低此任务的优先级,从而可能增加时间。 如果它不受CPU限制,我会省略让步。

基于这里的答案,我将使用最终实现来编写我的测试函数,而没有针对一般情况的单独时序。 如果我想获得其他统计数据,我会将其重新引入测试函数以及应用此处提到的其他内容。

我的第一个想法是一个简单的循环

 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个exception值)和标准差。 只有“中位数”统计信息要求您记录每次迭代。

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

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

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

 // 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),这不仅会降低整体基准速度,还会降低测量速度。 你的结果将更加“人为”,而不是反映你在现实世界中会得到什么。 也许更好的方法是避免在与基准测试同时运行其他任务。

无论你的函数计时机制如何(这里的答案似乎都很好),有一个非常简单的技巧来消除基准测试代码本身的开销,即循环开销,计时器读数和方法调用:

只需先用空的Func调用基准测试代码,即

 void EmptyFunc() {} 

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

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

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

  1. 基准空函数
  2. 计算结果的平均开销
  3. 基准真实的测试function
  4. 从这些测试结果中减去平均开销
  5. 你完成了

通过这样做,实际的计时机制突然变得不那么重要了。

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

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

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

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

HTH。

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

 static void Test2(string testName, Func 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(string testName, Func 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操作,但它仍然完成了工作。

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

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

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

我在这里有类似的问题 。

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

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

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

但要考虑的一件事是,CPU缓存未命中的影响是否真的是一个公平的事情来试图反击?

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

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

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