在C#中对小代码示例进行基准测试,是否可以改进此实现?

时间:2009-06-26 03:50:00

标签: c# .net performance profiling

经常这样我发现自己对小块代码进行基准测试,看看哪个实现最快。

我常常看到基准测试代码没有考虑到jitting或垃圾收集器的评论。

我有以下简单的基准测试功能,我已经慢慢演变了:

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

用法:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

此实施是否有任何缺陷?是否足以表明实现X比Z迭代实现Y更快?您能想出任何可以改善这种情况的方法吗?

修改 很明显,基于时间的方法(与迭代相反)是首选的,是否有人有任何时间检查不会影响性能的实现?

11 个答案:

答案 0 :(得分:91)

以下是修改后的功能:根据社区的建议,随时修改此社区维基。

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

确保在Release中编译并启用了优化,并在Visual Studio之外运行测试。最后一部分非常重要,因为即使在发布模式下,JIT也会附加调试器来优化其优化。

答案 1 :(得分:22)

GC.Collect返回之前,不一定要完成终结。终结排队,然后在单独的线程上运行。在测试过程中,该线程仍然可以处于活动状态,从而影响结果。

如果您想在开始测试之前确保完成最终化,那么您可能需要调用GC.WaitForPendingFinalizers,它将一直阻塞,直到清除完成队列:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

答案 2 :(得分:15)

如果您希望将GC交互排除在等式之外,您可能希望在 GC.Collect调用之后运行“预热”调用,而不是之前。这样你就知道.NET已经从操作系统为你的函数的工作集分配了足够的内存。

请记住,您正在为每次迭代进行非内联方法调用,因此请确保将要测试的内容与空体进行比较。你还必须接受,你只能可靠地计算比方法调用长几倍的东西。

此外,根据您正在分析的内容类型,您可能希望在一定时间内运行基于时间的运行而不是一定次数的迭代 - 它可能更容易导致 - 相比较的数字,无需为最佳实施而进行非常短的运行,和/或对于最差实施而言,非常长。

答案 3 :(得分:6)

我完全避免传递委托:

  1. 委托调用〜虚方法调用。不便宜:.NET中最小内存分配的~25%。如果您对详细信息感兴趣,请参阅e.g. this link
  2. 匿名代表可能会导致使用关闭,您甚至不会注意到。同样,访问关闭字段明显比例如访问堆栈上的变量。
  3. 导致关闭使用的示例代码:

    public void Test()
    {
      int someNumber = 1;
      Profiler.Profile("Closure access", 1000000, 
        () => someNumber + someNumber);
    }
    

    如果您不了解闭包,请在.NET Reflector中查看此方法。

答案 4 :(得分:6)

我认为像这样的基准测试方法要克服的最困难的问题是考虑边缘情况和意外情况。例如 - “两个代码片段如何在高CPU负载/网络使用/磁盘抖动/等情况下工作”。它们非常适合基本逻辑检查,以查看特定算法是否比其他算法更快地显着。但是要正确测试大多数代码性能,您必须创建一个测试来测量特定代码的特定瓶颈。

我仍然说测试小块代码通常几乎没有投资回报,并且可以鼓励使用过于复杂的代码而不是简单的可维护代码。编写清晰的代码,其他开发人员或我自己6个月后可以快速理解,比高度优化的代码具有更多的性能优势。

答案 5 :(得分:5)

我会多次致电func()进行热身,而不只是一次。

答案 6 :(得分:4)

改进建议

  1. 检测执行环境是否适合进行基准测试(例如检测是否附加了调试器或是否禁用了jit优化会导致测量结果不正确)。

  2. 独立测量代码的各个部分(以确切了解瓶颈的位置)。

  3. 比较不同的版本/组件/代码块(在你的第一句话中,你说'......对小块代码进行基准测试,看看哪个实现最快。')。
  4. 关于#1:

    • 要检测是否附加了调试器,请读取属性System.Diagnostics.Debugger.IsAttached(请记住还要处理最初未附加调试器但在一段时间后附加调试器的情况)。

      < / LI>
    • 要检测是否禁用了jit优化,请读取相关程序集的属性DebuggableAttribute.IsJITOptimizerDisabled

      private bool IsJitOptimizerDisabled(Assembly assembly)
      {
          return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
              .Select(customAttribute => (DebuggableAttribute) customAttribute)
              .Any(attribute => attribute.IsJITOptimizerDisabled);
      }
      

    关于#2:

    这可以通过多种方式完成。一种方法是允许提供几个代表,然后单独测量这些代表。

    关于#3:

    这也可以通过多种方式完成,不同的用例需要非常不同的解决方案。如果手动调用基准测试,那么写入控制台可能没问题。但是,如果构建系统自动执行基准测试,那么写入控制台可能就不那么好了。

    执行此操作的一种方法是将基准测试结果作为强类型对象返回,可以在不同的上下文中轻松使用。


    Etimo.Benchmarks

    另一种方法是使用现有组件来执行基准测试。实际上,在我的公司,我们决定将我们的基准工具发布到公共领域。在它的核心,它管理垃圾收集器,抖动,热身等,就像这里提出的其他一些答案。它还具有我上面提到的三个功能。它管理了Eric Lippert blog中讨论的几个问题。

    这是一个示例输出,其中比较了两个组件并将结果写入控制台。在这种情况下,比较的两个组件称为'KeyedCollection'和'MultiplyIndexedKeyedCollection':

    Etimo.Benchmarks - Sample Console Output

    有一个NuGet package,一个sample NuGet package,源代码位于GitHub。还有blog post

    如果您赶时间,我建议您获取示例包,并根据需要简单地修改示例代理。如果您不赶时间,最好阅读博客文章以了解详细信息。

答案 7 :(得分:1)

您还必须在实际测量之前运行“预热”传递,以排除JIT编译器在编写代码时花费的时间。

答案 8 :(得分:1)

根据您要进行基准测试的代码及其运行的平台,您可能需要考虑how code alignment affects performance。这样做可能需要一个外部包装器多次运行测试(在单独的应用程序域或进程中?),有些时候首先调用“填充代码”来强制它进行JIT编译,以便使代码成为以不同方式对齐的基准。完整的测试结果将为各种代码对齐提供最佳情况和最差情况时序。

答案 9 :(得分:1)

如果您尝试从基准测试中消除垃圾收集影响,是否值得设置GCSettings.LatencyMode

如果没有,并且您希望func中创建的垃圾的影响成为基准测试的一部分,那么您是否也应该在测试结束时(在计时器内)强制收集?< / p>

答案 10 :(得分:0)

你的问题的基本问题是假设单一 测量可以回答您的所有问题。你需要衡量 多次得到有效的情况和情况 特别是像C#这样的垃圾收集语言。

另一个答案提供了衡量基本表现的好方法。

static void Profile(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

然而,这种单一测量并不考虑垃圾 采集。适当的配置文件还可以解决最糟糕的情况 垃圾收集扩散到许多电话(这个数字是排序 无用的,因为虚拟机可以在没有收集剩余的情况下终止 垃圾,但仍然有用于比较两个不同的 func的实现。)

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

还有一个人可能想要衡量最糟糕的表现 对于只调用一次的方法的垃圾收集。

static void ProfileGarbage(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

但比推荐任何特定的可能附加物更重要 测量到轮廓是一个人应该测量倍数的想法 不同的统计数据,而不仅仅是一种统计数据。