在 C# 中对小代码示例进行基准测试,这个实现可以改进吗?
Posted
技术标签:
【中文标题】在 C# 中对小代码示例进行基准测试,这个实现可以改进吗?【英文标题】:Benchmarking small code samples in C#, can this implementation be improved? 【发布时间】:2009-06-26 03:50:00 【问题描述】:我发现自己经常对一小段代码进行基准测试,以查看哪个实现最快。
我经常看到基准代码不考虑jitting 或垃圾收集器的cmets。
我有以下简单的基准测试功能,我已经慢慢发展了:
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 快?你能想出什么方法来改进它吗?
编辑 很明显,基于时间的方法(而不是迭代)是首选,是否有人有任何实现时间检查不影响性能的实现?
【问题讨论】:
另见BenchmarkDotNet。 【参考方案1】:这是修改后的功能:根据社区的建议,请随时修改此社区 wiki。
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 之外运行测试。最后一部分很重要,因为 JIT 通过附加的调试器限制其优化,即使在发布模式下也是如此。
【讨论】:
您可能希望将循环展开若干次,例如 10 次,以尽量减少循环开销。 我刚刚更新为使用 Stopwatch.StartNew。不是功能上的改变,而是节省了一行代码。 @Luke,很大的改变(我希望我能 +1)。 @Mike 我不确定,我怀疑虚拟调用开销会比比较和分配高得多,因此性能差异可以忽略不计 我建议您将迭代计数传递给 Action,并在那里创建循环(可能 - 甚至展开)。如果您测量的是相对较短的操作,这是唯一的选择。而且我更喜欢看到逆度量 - 例如通过次数/秒。 您对显示平均时间有何看法。像这样: Console.WriteLine("Average Time Elapsed 0 ms", watch.ElapsedMilliseconds / iterations);【参考方案2】:最终确定不一定会在GC.Collect
返回之前完成。完成排队,然后在单独的线程上运行。该线程可能在您的测试期间仍然处于活动状态,从而影响结果。
如果您想在开始测试之前确保完成完成,那么您可能需要调用GC.WaitForPendingFinalizers
,这将阻塞直到完成队列被清除:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
【讨论】:
为什么还要GC.Collect()
?
@colinfang 因为被“终结”的对象没有被终结器 GC。所以第二个Collect
是为了确保“最终”对象也被收集。【参考方案3】:
如果您想将 GC 交互排除在等式之外,您可能希望在 GC.Collect 调用之后运行“热身”调用,而不是之前。这样你就知道 .NET 已经为你的函数的工作集从操作系统分配了足够的内存。
请记住,您正在为每次迭代进行非内联方法调用,因此请确保将您正在测试的内容与空主体进行比较。您还必须接受,您只能可靠地为比方法调用长几倍的事情计时。
此外,根据您要分析的内容类型,您可能希望基于计时运行一定时间而不是一定次数的迭代 - 这往往会导致更容易 -可比较的数字,而不必为最佳实施安排很短的时间和/或为最坏的实施安排很长的时间。
【讨论】:
好点,您是否考虑过基于时间的实施?【参考方案4】:我会完全避免传递委托:
-
委托调用是~虚拟方法调用。不便宜:大约 .NET 中最小内存分配的 25%。如果您对详细信息感兴趣,请参阅e.g. this link。
匿名委托可能会导致使用闭包,您甚至不会注意到这一点。同样,访问闭包字段明显比例如访问堆栈上的变量。
导致闭包使用的示例代码:
public void Test()
int someNumber = 1;
Profiler.Profile("Closure access", 1000000,
() => someNumber + someNumber);
如果您不了解闭包,请查看 .NET Reflector 中的此方法。
【讨论】:
有趣的点,但是如果您不传递委托,您将如何创建可重用的 Profile() 方法?还有其他方法可以将任意代码传递给方法吗? 我们使用“使用(新测量(...))...测量代码...”。所以我们得到实现 IDisposable 的测量对象,而不是传递委托。见code.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/… 这不会导致任何闭包问题。 @AlexYakunin:您的链接似乎已损坏。您能否在答案中包含测量类的代码?我怀疑无论您如何实现它,您都无法使用这种 IDisposable 方法多次运行要分析的代码。但是,在您想要测量复杂(交织)应用程序的不同部分如何执行的情况下,它确实非常有用,只要您记住测量可能不准确,并且在不同时间运行时不一致。我在大多数项目中都使用相同的方法。 多次运行性能测试的要求非常重要(热身+多次测量),所以我也切换到了委托方法。此外,如果您不使用闭包,则委托调用比使用IDisposable
的接口方法调用更快。【参考方案5】:
我认为像这样的基准测试方法最难克服的问题是考虑边缘情况和意外情况。例如 - “这两个代码 sn-ps 如何在高 CPU 负载/网络使用/磁盘抖动/等下工作。”它们非常适合进行基本逻辑检查,以查看特定算法是否明显比另一个更快。但是要正确测试大多数代码性能,您必须创建一个测试来测量该特定代码的特定瓶颈。
我仍然想说,测试小块代码通常没有什么投资回报,并且会鼓励使用过于复杂的代码而不是简单的可维护代码。编写其他开发人员或我自己 6 个月后可以快速理解的清晰代码将比高度优化的代码具有更多的性能优势。
【讨论】:
significant 是真正加载的术语之一。有时,实现速度提高 20% 很重要,有时它必须快 100 倍才有意义。同意你的清晰观点:***.com/questions/1018407/… 在这种情况下,重要的并不是所有的加载。您正在比较一个或多个并发实现,如果这两种实现的性能差异在统计上不显着,则不值得采用更复杂的方法。【参考方案6】:我会多次致电func()
进行热身,而不仅仅是一次。
【讨论】:
目的是确保执行 jit 编译,在测量之前多次调用 func 有什么好处? 让 JIT 有机会改进其第一个结果。 .NET JIT 不会随着时间的推移改善其结果(就像 Java 一样)。它只会在第一次调用时将方法从 IL 转换为 Assembly 一次。【参考方案7】:改进建议
检测执行环境是否适合进行基准测试(例如检测是否附加了调试器或是否禁用了 jit 优化,这会导致测量不正确)。
独立测量部分代码(准确查看瓶颈所在)。
比较不同版本/组件/代码块(在您的第一句话中,您说“...对小块代码进行基准测试以查看哪个实现最快。”)。关于#1:
要检测是否附加了调试器,请读取属性System.Diagnostics.Debugger.IsAttached
(请记住还要处理最初未附加调试器但一段时间后附加的情况)。
要检测是否禁用了 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”:
有一个NuGet package,一个sample NuGet package,源代码位于GitHub。还有一个blog post。
如果您赶时间,我建议您获取示例包并根据需要简单地修改示例代表。如果您不着急,最好阅读博文以了解详细信息。
【讨论】:
【参考方案8】:您还必须在实际测量之前运行“热身”通道,以排除 JIT 编译器花在 jitting 代码上的时间。
【讨论】:
在测量之前进行【参考方案9】:根据您进行基准测试的代码及其运行平台,您可能需要考虑how code alignment affects performance。这样做可能需要一个多次运行测试的外部包装器(在不同的应用程序域或进程中?),有些时候首先调用“填充代码”来强制它被 JIT 编译,从而导致代码被基准以不同方式对齐。完整的测试结果将为各种代码对齐提供最佳情况和最差情况的时序。
【讨论】:
【参考方案10】:如果您试图从基准测试中消除垃圾收集影响,是否值得设置GCSettings.LatencyMode
?
如果不是,并且您希望在 func
中创建的垃圾的影响成为基准的一部分,那么您不应该在测试结束时(在计时器内)强制收集吗?
【讨论】:
【参考方案11】:您的问题的基本问题是假设单个 测量可以回答你所有的问题。你需要测量 多次有效地了解情况并 尤其是在像 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);
但是,这个单一的测量并没有考虑到垃圾
收藏。适当的配置文件还解释了最坏情况下的性能
垃圾收集分布在许多调用中(这个数字是排序的
无用,因为 VM 可以终止而无需收集剩余的
垃圾,但仍可用于比较两个不同的
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);
但比推荐任何具体的可能的补充更重要 测量来描述是一种应该测量多个的想法 不同的统计数据,而不仅仅是一种统计数据。
【讨论】:
以上是关于在 C# 中对小代码示例进行基准测试,这个实现可以改进吗?的主要内容,如果未能解决你的问题,请参考以下文章