使用Benchmark.NET对C# 代码进行基准测试的简介
在我以前的文章中[10],我介绍了该系列文章[11],在其中我将分享我的经验,同时了解C#和.NET Core(corefx)框架的新性能。在本文中,我想着重于对现有代码进行基准测试并建立基准。
为什么要对C#代码进行基准测试?
我开始进行基准测试的原因是,在我们能够并且应该开始优化代码之前,我们应该首先了解我们的当前位置。这对于确保我们的变更正在产生我们所希望的影响,并且最重要的是不使我们的性能变差至关重要。以我的经验,性能工作是一个反复的过程或度量,需要进行小的更改并再次进行度量以检查更改的效果。
可以说,在本系列文章中我可能已经开始了其他一些工作,可能是通过概要分析,跟踪或度量收集。所有这些可能都是必需的,以便针对应优化的服务,并在代码级,类和方法上成为您的目标。
我决定暂时跳过这些更高级别的技术,部分原因是我对这些领域并不完全有信心,当然我可以为他们提供良好的指导。而且,它们是有关性能的一系列主题,我认为这会分散我对语言和框架功能的关注。
对于实际情况,您可能需要使用此类技术来首先缩小应该花费时间进行优化的位置。对于真实的场景,您可能需要首先使用这些技术来缩小您应该花时间优化的位置范围。有时可以做出正确的猜测,但只要有可能,最好是在你的努力中具有科学性,并以实际数据支持理论。我可能有一天会回到这些更广泛的领域,但是现在,我假设您对要改进的代码路径有所了解。
如果您确实想了解有关对代码进行概要分析的更多信息,那么我阅读了Konrad Kokosa的 “Pro .NET Memory Management: For Better Code, Performance, and Scalability[12]”这本书,并从中学到了很多东西。
基准测试是在代码的典型条件下确定当前性能的过程。在.NET中,在代码级别,有许多可行的技术。有时,使用简单的秒表将是收集常规计时数据的起点。
请注意,许多情况可能会影响您的测量及其准确性。秒表的优点是使用简单,可以快速提供结果。我认为以这种方式收集一些基本数据没有什么错,只要可以理解准确性的折衷即可。
一旦将注意力集中在代码的特定领域,您就会开始深入到方法级别。
在这一点上,开始为现有方法和代码记录更准确和特定的基准非常有用。这是基准测试应成为您选择的工具的地方。在C#中,我们有一个Benchmark.NET[13]形式的绝佳选择。该库提供了大量基准测试工具,可用于测量和基准测试.NET代码。现在,Microsoft团队经常使用Benchmark .NET来衡量其代码。
什么是基准?
基准仅仅是与某些代码的执行有关的一种测量或一组测量。通过基准测试,您可以在开始努力提高性能时比较代码的相对性能。
基准测试的范围可能很广,或者您经常会发现自己正在测试微观基准的微小变化。最主要的是确保您具有一种机制,可以将建议的更改与原始代码进行比较,从而指导优化工作。在优化代码时,使用数据而不是假设很重要。
如何对C#代码进行基准测试
希望到现在为止,您已经对基准的概念有所了解,所以让我们从一个简单的例子开始。如果您想继续阅读,可以在此示例存储库[14]的“基准”分支上找到此文章的完整代码。
public class NameParser{public string GetLastName(string fullName){var names = fullName.Split(" ");var lastName = names.LastOrDefault();return lastName ?? string.Empty;} }
假设我们已经确定以下NameParser是我们的应用程序在重负载和潜在性能瓶颈下的一个区域。 此代码是一个简单的实现,用于从输入字符串中返回姓氏,该字符串被假定为人的全名。就本演示而言,它假定最后一个单词,在任何空格代表姓氏之后。
目前,这只是一个简化的示例,您可能要进行基准测试的方法可能会完成更复杂的工作!有时,您可以从现有代码库中直接引用和基准测试代码,这些方法足够小且公开。在其他时候,我发现自己通过将代码的相关部分复制到我的基准测试项目中来创建基准测试,以便将重点放在特定的代码行上。
我需要在这方面花更多的时间来确定围绕构建基准的良好做法。
1、安装Benchmark.NET库
第一步是安装Benchmark.NET库。通常,因为您可能已经在进行单元测试,所以您将创建一个单独的项目来保存基准。在此基准测试项目中,您将引用包含要基准测试的代码的项目。为了使我的示例保持简单,我现在将所有内容留在一个项目中。
对于一般基准,您只需要NuGet的主要BenchmarkDotNet软件包。我通过在命令行中使用
“ dotnet add package BenchmarkDotNet –version 0.11.3”
将其添加到示例项目中来安装我的系统。
2、建立基准
下一步是通过创建一个包含基准的新类来创建基准基准测试类将由Benchmark.NET运行,并且任何基准测试方法的结果将包含在输出中。这是我的NameParserBenchmarks类。
[MemoryDiagnoser]public class NameParserBenchmarks{private const string FullName = "Steve J Gordon";private static readonly NameParser Parser = new NameParser();[Benchmark(Baseline = true)]public void GetLastName(){Parser.GetLastName(FullName);} }
类本身用BenchmarkDotNet.Attributes命名空间中的属性标记。Benchmark.NET具有诊断程序的概念,可以控制要测量并包含在结果中的事物。在没有附加任何附加诊断程序的情况下,它将仅提供基准数据的时序数据。内存诊断程序支持分配和GC收集的其他度量,这在优化代码时非常有帮助。
在前面的代码中,我有一个名为GetLastName的方法,该方法通过调用它对NameParser类中现有的GetLastName方法进行基准测试。我已经用Benchmark属性标记了此方法,以便Benchmark.NET执行该方法并将其包含在结果中。我可以在此处为基线属性提供一个值,以将该特定方法标记为基线。这是我们正在测量的现有代码,以后将很有用,因为所有其他基准都将与该初始代码进行比较。
为了支持基准测试,我在基准测试中包含了要解析的名称的静态字符串值。我还包括一个静态字段,其中包含对新NameParser实例的引用。我不想将它们包括在Benchmark方法本身中,因为我想单独测量GetLastName方法的性能和分配。
3、运行Benchmark.NET
最后一步是为Benchmark.NET设置并触发运行程序。在此示例中,我将运行单个项目中的所有内容,因此将更新Program类的Main方法。
通用的BenchmarkRunner.Run方法的调用接受应为其运行任何基准的类。默认情况下,基准测试结果将记录到控制台。
public class Program{public static void Main(string[] args){var summary = BenchmarkRunner.Run<NameParserBenchmarks>();}}
执行基准
在此阶段,我们准备运行基准测试。为了获得最佳结果,建议您在运行最少的设备上执行此操作。关闭所有其他应用程序并杀死不必要的进程将产生最稳定的结果。在开发机器上,一旦一切都关闭,我将触发从命令行运行基准测试。
应针对发布代码运行基准测试,以确保包括所有优化。在我的项目目录中,我将运行
“ dotnet build -c Release”
以创建一个发布版本。
构建完成后,我可以导航到包含构建代码的文件夹:“ cd bin / Release / netcoreapp2.2”
最后,我可以通过对示例应用程序使用“ dotnet BenchmarkAndSpanExample.dll”运行构建的程序集来运行基准测试。
运行基准测试所需的时间长短取决于您的计算机和受测试的代码。Benchmark.NET执行许多阶段来预热代码,并确保运行多次迭代以提供一致的统计数据。它使用一个试验阶段来计算要运行的最佳迭代次数,尽管您可以根据需要进行配置。
解释结果
完成后,您应该将摘要结果写入控制台窗口。如果愿意,可以在运行应用程序的位置下的BenchmarkDotNet.Artifacts文件夹中生成各种输出。其中包括摘要的HTML版本,可以更轻松地共享。
我的机器的摘要如下所示:
对于每种基准测试方法,您将在一行中包含结果数据。在这里,我只有一行用于我的GetLastName方法的基准测试。平均执行时间为125.8纳秒;不是太寒酸!其他统计数据可用于迭代中时序数据的误差和标准偏差。
因为我包括了memory diagnosticr属性,所以我包括了一些额外的列,其中包含与内存相关的统计信息。前三列与GC集合有关。它们按比例缩放以显示每1,000个操作的数量。在这种情况下,必须经常调用我的方法才能触发Gen 0集合,并且不太可能导致Gen 1或Gen 2集合。最后一栏非常有帮助,它显示了每个操作分配的内存。我的名称解析器代码当前每次被分配160个字节。在宏伟的计划中,这根本不算什么,但是我们将在以后的文章中看到如何减少这种情况。请记住,尽管.NET中的分配很便宜,但GC收集和清理这些对象的工作可能会带来更多影响。在热路径(被称为方法)中,这很快就会加起来。
在我的第一篇文章中[15],我提到了一个工人流程,该流程每天维护17至2000万个事件。如果在处理每个事件时需要调用此GetLastName方法,则每天将导致3.2GB的分配!如此规模如此之小,很快就可以加起来!
摘要
在尝试对代码进行任何优化工作之前,始终先建立基线是非常重要且重要的。这样,您可以真正看到改进后的代码是否比原始代码更快和/或分配的更少。
评估改进可以帮助指导进一步的优化,并且还可以提供关键数据,这些数据可以证明花费时间进行此类改进的代码。使用Benchmark.NET之类的工具进行基准测试非常简单,只需进行简单的工作,几乎不需要花什么功夫,就可以轻松比较代码性能。
在本文中,我们已经了解了如何使用Benchmark .NET为现有代码提供基线,以了解其运行速度和分配的内存量。在下一篇文章中,我将介绍Span,我们将使用Benchmark .NET来衡量改进。
谢谢阅读!如果您想了解有关高性能.NET和C#代码的更多信息,可以在此处[16]查看我的完整博客文章系列。
References
[1]
Writing High-Performance .NET Code: https://www.amazon.co.uk/gp/product/0990583457/ref=as_li_tl?ie=UTF8&camp=1634&creative=6738&creativeASIN=0990583457&linkCode=as2&tag=stevejgordon-21&linkId=9345b81c7b89459a2015a61e7470abb9[2]
编写高性能.NET代码: https://item.jd.com/15217405980.html[3]
Pro .NET Memory Management: For Better Code, Performance, and Scalability: https://www.amazon.com/Pro-NET-Memory-Management-Performance/dp/148424026X/ref=sr_1_1?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&keywords=.NET+Memory+Management&qid=1583662848&sr=8-1[4]
Blogs: https://adamsitnik.com/[5]
talks: https://www.youtube.com/watch?v=CSPSvBeqJ9c[6]
博客: https://adamsitnik.com/[7]
演讲: https://www.youtube.com/watch?v=CSPSvBeqJ9c[8]
博客: https://blog.marcgravell.com/[9]
在此处: https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code[10]
以前的文章中: https://www.stevejgordon.co.uk/motivations-for-writing-high-performance-csharp-code[11]
文章: https://www.stevejgordon.co.uk/motivations-for-writing-high-performance-csharp-code[12]
Pro .NET Memory Management: For Better Code, Performance, and Scalability: https://www.amazon.co.uk/gp/product/148424026X/ref=as_li_tl?ie=UTF8&camp=1634&creative=6738&creativeASIN=148424026X&linkCode=as2&tag=stevejgordon-21&linkId=fc3f451494b7fdcefdfa03674f1cd2da[13]
Benchmark.NET: https://benchmarkdotnet.org/[14]
此示例存储库: https://github.com/stevejgordon/BenchmarkAndSpanExample/tree/Benchmarks[15]
第一篇文章中: https://www.stevejgordon.co.uk/motivations-for-writing-high-performance-csharp-code[16]
在此处: https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code