C#: 这个基准测试类准确吗?

35

我创建了一个简单的类来测试我的一些方法,但它准确吗?我对基准测试、计时等方面还比较陌生,所以想在这里寻求一些反馈。如果它很好用,也许其他人也可以使用它 :)

public static class Benchmark
{
    public static IEnumerable<long> This(Action subject)
    {
        var watch = new Stopwatch();
        while (true)
        {
            watch.Reset();
            watch.Start();
            subject();
            watch.Stop();
            yield return watch.ElapsedTicks;
        }
    }
}

您可以像这样使用它:

var avg = Benchmark.This(() => SomeMethod()).Take(500).Average();

有任何反馈吗?它看起来相当稳定和准确,还是我漏了什么?


+1 非常原创的实现。我将尝试它来衡量它是否足够准确,以适应我所做的即兴测试类型。谢谢! - Zach Bonham
你不能只使用var avg = Benchmark.This(SomeMethod).Take(500).Average();而不是使用lambda表达式吗? - Lucas Jones
1
在这个例子中,实际上我可以这样做。只是这样写可以更清楚地表明我正在发送一个可能是lambda或其他任何东西的Action。 - Svish
如果您正在使用VS 2010 beta2,并将Svish的代码复制到通过Project / Add Class创建的新类中,请注意该类不会自动添加对using System.Collections的引用...您需要手动添加。 - BillW
你可以使用这个开源框架:BenchmarkDotNet。它包括了计时器的使用、GC 预调用、预热、设置进程、线程、处理器亲和度掩码、基准测试竞赛 API 和漂亮的控制台输出结果。 - AndreyAkinshin
4个回答

21

这是一个简单基准测试的最高精度了。但是有一些因素不在您的控制范围内:

  • 来自其他进程的系统负载
  • 基准测试之前/期间堆的状态

您可以对最后一点做些调整,基准测试是罕见的情况之一,可以辩称调用GC.Collect。并且您可能会先调用subject一次以消除任何JIT问题。但这要求对subject的调用是独立的。

public static IEnumerable<TimeSpan> This(Action subject)
{
    subject();     // warm up
    GC.Collect();  // compact Heap
    GC.WaitForPendingFinalizers(); // and wait for the finalizer queue to empty

    var watch = new Stopwatch();
    while (true)
    {
        watch.Reset();
        watch.Start();
        subject();
        watch.Stop();
        yield return watch.Elapsed;  // TimeSpan
    }
}

对于奖金来说,你的类应该检查System.Diagnostics.Stopwatch.IsHighResolution字段。如果关闭,你只有非常粗糙的(20毫秒)分辨率。

但在一台普通PC上,由于运行了许多后台服务,它永远不会非常准确。


3
在进行垃圾回收后,等待挂起的终结器并非坏主意。请记住,终结器在不同的线程上运行; 当前运行时,之前运行的终结器可能正在另一个线程上运行。 - Eric Lippert
所有的好主意。我在我的类中已经实现了它们。唯一改变的是返回TimeSpan而不是ElapsedMilliseconds或者Ticks :) - Svish

10

这里有几个问题。

首先,请记住第一次运行代码时,它的方法调用的传递闭包将被JIT编译。这意味着第一次运行很可能比每个后续运行都更耗费时间。根据您是基准测试“冷”时间还是“热”时间,这可能会有所不同。我见过某些方法,方法的JIT成本甚至高于所有其他调用该方法的成本总和!

其次,请记住垃圾收集器在另一个线程上运行。如果您在一次运行中产生垃圾,则清理该垃圾的成本可能不会在随后的运行中实现。因此,您未能考虑一次运行的总成本,而是将其转移为后续运行。

这两者都表明了基准测试的弱点:基准测试本质上是不现实的,因此价值有限。在实际代码中,GC将正在运行,JIT将正在运行等等。经常情况下,基准测试性能与现实世界的性能完全不同,因为基准测试没有考虑到大型系统固有的真实成本变化。与其孤立地分析性能特征,我更喜欢查看真正客户面临的实际场景的性能特征。


7
你应该返回ElapsedMilliseconds而不是ElapsedTicks。ElapsedTicks返回值取决于Stopwatch频率,不同系统上的频率可能不同。它不一定对应于Timespan或DateTime对象的Ticks属性。
参见http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.elapsedticks.aspx
如果您确实想要更高分辨率的Ticks,则应返回watch.Elapsed.Ticks(即Timestamp.Ticks)而不是watch.ElapsedTicks(这可能是.NET中最微妙的潜在错误之一)。来自MSDN:
Stopwatch滴答声与DateTime.Ticks不同。 DateTime.Ticks值中的每个滴答声表示100纳秒间隔。 ElapsedTicks值中的每个滴答声表示等于Frequency除以1秒的时间间隔。
除此之外,我认为你的代码很好,尽管我认为你在测量中包含了一些方法调用开销,如果方法本身执行时间非常短,则这可能是重要的。另外,您可能希望从计算出的平均值中排除对方法的第一次调用,但我不确定您将如何在类中实现这一点。
最后一个观点,对于大多数使用该类的情况可能不相关:秒表与系统时间相比略快。 在我的电脑上,经过24小时后,它会领先约5秒钟(这是,而不是毫秒),在其他机器上,这种漂移可能更大。 因此,说它高度准确有点误导人,实际上只是高度粒度。 对于计时短持续时间的方法,这显然不会是一个重要问题。

还有一个非常重要的点需要说明:在进行基准测试时,我经常会发现一些运行时间都集中在一个狭窄的值范围内(例如80、80、79、82等),但偶尔在Windows上会发生其他事情(例如打开另一个程序或我的杀毒软件启动了什么),导致其中一个值与其他值相差甚远(例如80、80、79、271、80等)。我认为解决这个异常值问题的简单方法是使用测量值的中位数而不是平均数。我不知道Linq是否自动支持这个功能。


1
+1 是为了捕捉 ElapsedTicks 和 Elapsed.Ticks 的区别。另外,你尝试过使用百分位数来筛选“边缘”情况吗?例如,90% 的迭代次数在阈值以下或等于阈值。请参见 http://www.techbookreport.com/tutorials/quantiles.html,虽然我会作弊,只使用 Excel。 :) - Zach Bonham
1
@Joren:使用ElapsedTicks并不一定是错误。然而,由于这是一个用于基准测试的类,如果该类的用户将ElapsedTicks值转储到TimeSpan中以计算经过的持续时间,那么它几乎肯定会成为一个错误。我应该说“这可能是.NET中最微妙的潜在错误之一”。事实上,我这样说。 - MusiGenesis
也许直接返回经过的时间间隔本身会更有意义? - Svish
@Svish:使用yield会起作用吗?我只需返回ElapsedMilliseconds-无论如何,您永远不会真正获得亚毫秒的精度,无论您如何做到这一点。 - MusiGenesis
"ElapsedTotalMilliseconds"提供比"ElapsedMilliseconds"更精确的结果。除此之外,我通常会选择mode,但平均值也要加1 :) - nawfal
显示剩余6条评论

2
作为一名非C#程序员,我无法准确地说这个类是否适合用于计算函数执行时间。然而,有一些需要记住的事情以保证可重复性和准确性。
我不太了解.NET框架的各种细节,但是根据它如何编译成本地代码,任何编译都可能影响基准测试结果。此外,函数是否在缓存中也会产生影响。因此,您需要循环运行您的函数,以确保没有编译问题,并且一切都已经加载并准备好了。完成这些步骤后,您可能就可以开始了。
其他人可能比我更了解.NET的相关信息和知识。

通常编译的 .Net 应用程序不是本地 EXE,但你提到的所有点都基本正确。在 Visual Studio 的调试模式下运行的应用程序通常比正常启动的 .Net EXE 运行速度慢,并且函数通常在第一次调用时运行速度较慢(特别是如果它们调用其他必须加载的程序集),因此第一次调用方法时最好不要将其运行时间包括在平均测量中。 - MusiGenesis
好主意,应该编辑它以便在开始产生结果之前运行一次方法 :) - Svish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接