在C#中对小代码样本进行基准testing,可以改进这个实现吗?

所以我经常发现我自己testing了一小段代码,看看哪个实现是最快的。

通常我会看到基准testing代码没有考虑到jitting或垃圾回收器的评论。

我有以下简单的基准function,我已经慢慢演变:

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 }); 

这个实现是否有缺陷? 是否足以certificate在Z迭代中实现X比实现Y更快? 你能想出任何可以改进的方法吗?

编辑它非常清楚,基于时间的方法(而不是迭代),是首选,有没有人有任何实施的时间检查不影响性能?

这里是修改后的function:如社区推荐,随意修改它的社区维基。

 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; } 

确保您在启用优化的情况下进行编译,然后在Visual Studio之外运行testing 。 最后一部分很重要,因为即使在发布模式下,JIT也会使用附加的debugging器来优化其优化。

GC.Collect返回之前,结束不一定会完成。 完成排队,然后在一个单独的线程上运行。 在testing过程中,此线程仍可能处于活动状态,从而影响结果。

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

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

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

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

另外,取决于你正在分析的是什么样的东西,你可能想要基于你的时间运行一定的时间,而不是一定数量的迭代 – 它可能会导致更容易的比较数字为了最好的实施,必须有一个非常短的时间,或者是最糟糕的一个很长的时间。

我会避免通过代表:

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

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

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

如果你不知道closures,请看一下.NET Reflector中的这个方法。

我认为像这样的基准testing方法要解决的最困难的问题是考虑边缘情况和意外情况。 例如 – “这两个代码片断如何在高CPU负载/networking使用/磁盘抖动等情况下工作”。 对于基本的逻辑检查来说,它们是非常好的,以查看特定的algorithm是否比其他algorithm快得多 但是要正确地testing大多数代码的性能,你必须创build一个testing来衡量特定代码的特定瓶颈。

我仍然认为testing小块代码往往没有什么投资回报,并且可以鼓励使用过于复杂的代码而不是简单的可维护代码。 编写明确的代码,其他开发人员或者我自己6个月后能够快速理解的代码比高度优化的代码具有更多的性能优势。

我会打几次func()来进行热身,而不是一次。

改进build议

  1. 检测执行环境是否适合进行基准testing(例如,检测是否附加了debugging器或禁用了jit优化,从而导致不正确的测量)。

  2. 单独测量部分代码(准确查看瓶颈的位置)。

  3. 比较不同的版本/组件/代码块(在你的第一句话中,你会说'…对小块代码进行基准testing以查看哪个实现最快“)。

关于#1:

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

  • 要检测是否禁用jit优化,请阅读相关程序集的属性DebuggableAttribute.IsJITOptimizerDisabled:

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

关于#2:

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

关于#3:

这也可以通过很多方式来完成,不同的用例需要不同的解决scheme。 如果基准是手动调用,那么写入控制台可能没问题。 但是,如果基准testing是由构build系统自动执行的,那么写入控制台可能不太好。

一种方法是将基准testing结果作为强types对象返回,可以在不同的上下文中轻松使用。


Etimo.Benchmarks

另一种方法是使用现有的组件来执行基准。 实际上,在我的公司,我们决定将我们的基准工具发布到公有领域。 它的核心是它pipe理垃圾收集器,抖动,热身等,就像这里提供的其他答案一样。 它也有我上面提到的三个特点。 它pipe理Eric Lippert博客中讨论的几个问题。

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

Etimo.Benchmarks  - 示例控制台输出

有一个NuGet包 ,一个样例的NuGet包 ,源代码可以在GitHub上find 。 还有一个博客post 。

如果你很急,我build议你得到样本包,并根据需要修改样本代表。 如果你不着急,阅读博客post了解细节可能是一个好主意。

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

根据基准testing代码和运行平台,您可能需要考虑代码alignment如何影响性能 。 要做到这一点可能需要多次运行testing的外包装(在单独的应用程序域或进程?),有些时候首先调用“填充代码”强制它被JIT编译,以便导致代码基准进行了不同的调整。 一个完整的testing结果会给出各种代码alignment的最佳情况和最坏情况的时间。

如果您试图从基准testing中完全消除垃圾收集的影响,那么是否值得设置GCSettings.LatencyMode

如果没有,并且你希望在func创build的垃圾的影响成为基准的一部分,那么你是否也应该在testing结束时(定时器内)强制收集?

你的问题的基本问题是假设一个单一的测量可以回答你所有的问题。 您需要多次测量以获得有关情况的有效图片,尤其是像C#这样的垃圾收集语言。

另一个答案给出了衡量基本performance的好方法。

 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); } 

但是,这种单一的测量不包括垃圾收集。 一个合适的configuration文件还会考虑垃圾收集在很多调用中传播的最坏情况(这个数字是无用的,因为虚拟机可以终止而不会收集剩余的垃圾,但仍然可以用来比较两种不同的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); } 

但是,比推荐任何具体可能的附加测量方法更重要的是,应该测量多种不同的统计数据而不仅仅是一种统计数据。