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

Sam*_*ron 104 .net c# 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);
    }
Run Code Online (Sandbox Code Playgroud)

用法:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});
Run Code Online (Sandbox Code Playgroud)

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

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

Sam*_*ron 93

这是修改后的功能:根据社区的建议,随意修改它的社区维基.

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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我刚刚更新为使用Stopwatch.StartNew.不是功能上的改变,但保存了一行代码. (2认同)
  • 您如何看待平均时间.这样的事情:Console.WriteLine("平均时间经过{0} ms",watch.ElapsedMilliseconds/iterations); (2认同)

Luk*_*keH 22

最终确定不一定在GC.Collect返回之前完成.终结排队,然后在单独的线程上运行.在测试期间,此线程仍可能处于活动状态,从而影响结果.

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

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Run Code Online (Sandbox Code Playgroud)

  • 为什么`GC.Collect()`再一次? (10认同)
  • @colinfang因为终结器没有对GC进行"最终化".所以第二个`Collect`就是为了确保收集到的"最终"对象. (7认同)

Jon*_*upp 15

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

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

此外,根据您正在分析的内容类型,您可能希望基于运行一段时间而不是进行一定数量的迭代 - 这可能会导致更容易比较的数字必须有一个非常短的运行以实现最佳实施和/或非常长的运行以实现最佳实施.


Ale*_*nin 6

我完全避免传递代表:

  1. 委托调用是〜虚方法调用.不便宜:.NET中最小内存分配的~25%.如果您对细节感兴趣,请参阅此链接.
  2. 匿名代表可能会导致使用闭包,您甚至不会注意到.同样,访问闭包字段明显比例如访问堆栈上的变量.

导致闭包使用的示例代码:

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

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

  • @AlexYakunin:您的链接似乎已损坏.你可以在答案中包含Measurement类的代码吗?我怀疑无论你如何实现它,你都无法使用这种IDisposable方法运行代码来多次分析.但是,在您想要测量复杂(交织)应用程序的不同部分如何执行的情况下,它确实非常有用,只要您记住测量可能不准确,并且在不同时间运行时不一致.我在大多数项目中使用相同的方法. (3认同)

Pau*_*der 6

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

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


Ale*_*nov 5

我会func()多次打电话给热身,而不仅仅是一次.

  • 让JIT有机会改善其第一批结果. (3认同)