如何编写高性能的C#代码(二)
使用Benchmark.NET對C# 代碼進行基準測試的簡介
在我以前的文章中[10],我介紹了該系列文章[11],在其中我將分享我的經(jīng)驗,同時了解C#和.NET Core(corefx)框架的新性能。在本文中,我想著重于對現(xiàn)有代碼進行基準測試并建立基準。
為什么要對C#代碼進行基準測試?
我開始進行基準測試的原因是,在我們能夠并且應(yīng)該開始優(yōu)化代碼之前,我們應(yīng)該首先了解我們的當(dāng)前位置。這對于確保我們的變更正在產(chǎn)生我們所希望的影響,并且最重要的是不使我們的性能變差至關(guān)重要。以我的經(jīng)驗,性能工作是一個反復(fù)的過程或度量,需要進行小的更改并再次進行度量以檢查更改的效果。
可以說,在本系列文章中我可能已經(jīng)開始了其他一些工作,可能是通過概要分析,跟蹤或度量收集。所有這些可能都是必需的,以便針對應(yīng)優(yōu)化的服務(wù),并在代碼級,類和方法上成為您的目標(biāo)。
我決定暫時跳過這些更高級別的技術(shù),部分原因是我對這些領(lǐng)域并不完全有信心,當(dāng)然我可以為他們提供良好的指導(dǎo)。而且,它們是有關(guān)性能的一系列主題,我認為這會分散我對語言和框架功能的關(guān)注。
對于實際情況,您可能需要使用此類技術(shù)來首先縮小應(yīng)該花費時間進行優(yōu)化的位置。對于真實的場景,您可能需要首先使用這些技術(shù)來縮小您應(yīng)該花時間優(yōu)化的位置范圍。有時可以做出正確的猜測,但只要有可能,最好是在你的努力中具有科學(xué)性,并以實際數(shù)據(jù)支持理論。我可能有一天會回到這些更廣泛的領(lǐng)域,但是現(xiàn)在,我假設(shè)您對要改進的代碼路徑有所了解。
如果您確實想了解有關(guān)對代碼進行概要分析的更多信息,那么我閱讀了Konrad Kokosa的 “Pro .NET Memory Management: For Better Code, Performance, and Scalability[12]”這本書,并從中學(xué)到了很多東西。
基準測試是在代碼的典型條件下確定當(dāng)前性能的過程。在.NET中,在代碼級別,有許多可行的技術(shù)。有時,使用簡單的秒表將是收集常規(guī)計時數(shù)據(jù)的起點。
請注意,許多情況可能會影響您的測量及其準確性。秒表的優(yōu)點是使用簡單,可以快速提供結(jié)果。我認為以這種方式收集一些基本數(shù)據(jù)沒有什么錯,只要可以理解準確性的折衷即可。
一旦將注意力集中在代碼的特定領(lǐng)域,您就會開始深入到方法級別。
在這一點上,開始為現(xiàn)有方法和代碼記錄更準確和特定的基準非常有用。這是基準測試應(yīng)成為您選擇的工具的地方。在C#中,我們有一個Benchmark.NET[13]形式的絕佳選擇。該庫提供了大量基準測試工具,可用于測量和基準測試.NET代碼。現(xiàn)在,Microsoft團隊經(jīng)常使用Benchmark .NET來衡量其代碼。
什么是基準?
基準僅僅是與某些代碼的執(zhí)行有關(guān)的一種測量或一組測量。通過基準測試,您可以在開始努力提高性能時比較代碼的相對性能。
基準測試的范圍可能很廣,或者您經(jīng)常會發(fā)現(xiàn)自己正在測試微觀基準的微小變化。最主要的是確保您具有一種機制,可以將建議的更改與原始代碼進行比較,從而指導(dǎo)優(yōu)化工作。在優(yōu)化代碼時,使用數(shù)據(jù)而不是假設(shè)很重要。
如何對C#代碼進行基準測試
希望到現(xiàn)在為止,您已經(jīng)對基準的概念有所了解,所以讓我們從一個簡單的例子開始。如果您想繼續(xù)閱讀,可以在此示例存儲庫[14]的“基準”分支上找到此文章的完整代碼。
public class NameParser{public string GetLastName(string fullName){var names = fullName.Split(" ");var lastName = names.LastOrDefault();return lastName ?? string.Empty;} }假設(shè)我們已經(jīng)確定以下NameParser是我們的應(yīng)用程序在重負載和潛在性能瓶頸下的一個區(qū)域。?此代碼是一個簡單的實現(xiàn),用于從輸入字符串中返回姓氏,該字符串被假定為人的全名。就本演示而言,它假定最后一個單詞,在任何空格代表姓氏之后。
目前,這只是一個簡化的示例,您可能要進行基準測試的方法可能會完成更復(fù)雜的工作!有時,您可以從現(xiàn)有代碼庫中直接引用和基準測試代碼,這些方法足夠小且公開。在其他時候,我發(fā)現(xiàn)自己通過將代碼的相關(guān)部分復(fù)制到我的基準測試項目中來創(chuàng)建基準測試,以便將重點放在特定的代碼行上。
我需要在這方面花更多的時間來確定圍繞構(gòu)建基準的良好做法。
1、安裝Benchmark.NET庫
第一步是安裝Benchmark.NET庫。通常,因為您可能已經(jīng)在進行單元測試,所以您將創(chuàng)建一個單獨的項目來保存基準。在此基準測試項目中,您將引用包含要基準測試的代碼的項目。為了使我的示例保持簡單,我現(xiàn)在將所有內(nèi)容留在一個項目中。
對于一般基準,您只需要NuGet的主要BenchmarkDotNet軟件包。我通過在命令行中使用
“ dotnet add package BenchmarkDotNet –version 0.11.3”將其添加到示例項目中來安裝我的系統(tǒng)。
2、建立基準
下一步是通過創(chuàng)建一個包含基準的新類來創(chuàng)建基準基準測試類將由Benchmark.NET運行,并且任何基準測試方法的結(jié)果將包含在輸出中。這是我的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命名空間中的屬性標(biāo)記。Benchmark.NET具有診斷程序的概念,可以控制要測量并包含在結(jié)果中的事物。在沒有附加任何附加診斷程序的情況下,它將僅提供基準數(shù)據(jù)的時序數(shù)據(jù)。內(nèi)存診斷程序支持分配和GC收集的其他度量,這在優(yōu)化代碼時非常有幫助。
在前面的代碼中,我有一個名為GetLastName的方法,該方法通過調(diào)用它對NameParser類中現(xiàn)有的GetLastName方法進行基準測試。我已經(jīng)用Benchmark屬性標(biāo)記了此方法,以便Benchmark.NET執(zhí)行該方法并將其包含在結(jié)果中。我可以在此處為基線屬性提供一個值,以將該特定方法標(biāo)記為基線。這是我們正在測量的現(xiàn)有代碼,以后將很有用,因為所有其他基準都將與該初始代碼進行比較。
為了支持基準測試,我在基準測試中包含了要解析的名稱的靜態(tài)字符串值。我還包括一個靜態(tài)字段,其中包含對新NameParser實例的引用。我不想將它們包括在Benchmark方法本身中,因為我想單獨測量GetLastName方法的性能和分配。
3、運行Benchmark.NET
最后一步是為Benchmark.NET設(shè)置并觸發(fā)運行程序。在此示例中,我將運行單個項目中的所有內(nèi)容,因此將更新Program類的Main方法。
通用的BenchmarkRunner.Run方法的調(diào)用接受應(yīng)為其運行任何基準的類。默認情況下,基準測試結(jié)果將記錄到控制臺。
public class Program{public static void Main(string[] args){var summary = BenchmarkRunner.Run<NameParserBenchmarks>();}}執(zhí)行基準
在此階段,我們準備運行基準測試。為了獲得最佳結(jié)果,建議您在運行最少的設(shè)備上執(zhí)行此操作。關(guān)閉所有其他應(yīng)用程序并殺死不必要的進程將產(chǎn)生最穩(wěn)定的結(jié)果。在開發(fā)機器上,一旦一切都關(guān)閉,我將觸發(fā)從命令行運行基準測試。
應(yīng)針對發(fā)布代碼運行基準測試,以確保包括所有優(yōu)化。在我的項目目錄中,我將運行
“ dotnet build -c Release”
以創(chuàng)建一個發(fā)布版本。
構(gòu)建完成后,我可以導(dǎo)航到包含構(gòu)建代碼的文件夾:“ cd bin / Release / netcoreapp2.2”
最后,我可以通過對示例應(yīng)用程序使用“ dotnet BenchmarkAndSpanExample.dll”運行構(gòu)建的程序集來運行基準測試。
運行基準測試所需的時間長短取決于您的計算機和受測試的代碼。Benchmark.NET執(zhí)行許多階段來預(yù)熱代碼,并確保運行多次迭代以提供一致的統(tǒng)計數(shù)據(jù)。它使用一個試驗階段來計算要運行的最佳迭代次數(shù),盡管您可以根據(jù)需要進行配置。
解釋結(jié)果
完成后,您應(yīng)該將摘要結(jié)果寫入控制臺窗口。如果愿意,可以在運行應(yīng)用程序的位置下的BenchmarkDotNet.Artifacts文件夾中生成各種輸出。其中包括摘要的HTML版本,可以更輕松地共享。
我的機器的摘要如下所示:
圖片對于每種基準測試方法,您將在一行中包含結(jié)果數(shù)據(jù)。在這里,我只有一行用于我的GetLastName方法的基準測試。平均執(zhí)行時間為125.8納秒;不是太寒酸!其他統(tǒng)計數(shù)據(jù)可用于迭代中時序數(shù)據(jù)的誤差和標(biāo)準偏差。
因為我包括了memory diagnosticr屬性,所以我包括了一些額外的列,其中包含與內(nèi)存相關(guān)的統(tǒng)計信息。前三列與GC集合有關(guān)。它們按比例縮放以顯示每1,000個操作的數(shù)量。在這種情況下,必須經(jīng)常調(diào)用我的方法才能觸發(fā)Gen 0集合,并且不太可能導(dǎo)致Gen 1或Gen 2集合。最后一欄非常有幫助,它顯示了每個操作分配的內(nèi)存。我的名稱解析器代碼當(dāng)前每次被分配160個字節(jié)。在宏偉的計劃中,這根本不算什么,但是我們將在以后的文章中看到如何減少這種情況。請記住,盡管.NET中的分配很便宜,但GC收集和清理這些對象的工作可能會帶來更多影響。在熱路徑(被稱為方法)中,這很快就會加起來。
在我的第一篇文章中[15],我提到了一個工人流程,該流程每天維護17至2000萬個事件。如果在處理每個事件時需要調(diào)用此GetLastName方法,則每天將導(dǎo)致3.2GB的分配!如此規(guī)模如此之小,很快就可以加起來!
摘要
在嘗試對代碼進行任何優(yōu)化工作之前,始終先建立基線是非常重要且重要的。這樣,您可以真正看到改進后的代碼是否比原始代碼更快和/或分配的更少。
評估改進可以幫助指導(dǎo)進一步的優(yōu)化,并且還可以提供關(guān)鍵數(shù)據(jù),這些數(shù)據(jù)可以證明花費時間進行此類改進的代碼。使用Benchmark.NET之類的工具進行基準測試非常簡單,只需進行簡單的工作,幾乎不需要花什么功夫,就可以輕松比較代碼性能。
在本文中,我們已經(jīng)了解了如何使用Benchmark .NET為現(xiàn)有代碼提供基線,以了解其運行速度和分配的內(nèi)存量。在下一篇文章中,我將介紹Span,我們將使用Benchmark .NET來衡量改進。
謝謝閱讀!如果您想了解有關(guān)高性能.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
總結(jié)
以上是生活随笔為你收集整理的如何编写高性能的C#代码(二)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Istio 2020 年 Roadmap
- 下一篇: 如何编写高性能的C#代码(一)