使用Benchmark .NET對C#代碼進行基準測試C#性能之旅

使用Benchmark .NET對C#代碼進行基準測試C#性能之旅 - 第2部分

為何選擇基準代碼?

我開始進行基準測試的原因是,在我們能夠並且應該開始優化代碼之前,我們應該首先了解我們當前的位置。這對於我們驗證我們的變化是否具有我們所希望的影響至關重要,最重要的是,不會使我們的表現更糟。根據我的經驗,績效工作是一個非常迭代的過程或測量,進行小的改變並再次測量以檢查變化的影響。

可以說,我可以在本系列中開始其他地方,可能還有分析,跟蹤或指標收集。所有這些可能都是必要的,以便針對應該優化的服務以及代碼級別,應該是您的目標的類和方法。我現在決定跳過這些更高級別的技術,部分原因是因為它們是我不完全自信的領域,當然是我能夠為他們提供良好指導的水平。此外,它們是他們自己的大量主題,我覺得這會分散我對語言和框架功能的關注。

對於真實場景,您可能需要使用此類技術來首先縮小您應該花時間優化的地方。有時可以做出好的猜測,但只要有可能,最好是在你的努力中保持科學,並用實際數據支持理論。我有一天可能會回到這些更廣泛的領域,但就目前而言,我將假設您對要改進的代碼路徑有所瞭解。如果您想了解有關分析代碼的更多信息,我從Konrad Kokosa 閱讀“ Pro .NET內存管理:更好的代碼,性能和可伸縮性 ” 中學到了很多東西。

基線是在代碼的典型條件下建立當前性能的過程。在.NET中,在代碼級別,有許多技術可以使用。有時,使用簡單的秒錶將是收集一般時間數據的起點。請注意,許多條件可能會影響您的測量及其準確性。一個好處是秒錶使用簡單,可以提供快速的結果。只要能夠理解準確性的妥協,我認為以這種方式收集一些基本數據並沒有錯。

一旦您將注意力集中到代碼的特定區域,您就開始深入到方法級別。此時,開始為現有方法和代碼記錄更準確和特定的基準測試非常有用。這是基準測試應該成為您的首選工具。在C#中,我們以Benchmark.NET的形式提供了一個很棒的選擇。該庫提供了大量基準測試工具,可用於測量和測試.NET代碼。Microsoft的團隊現在經常使用Benchmark .NET來測量他們的代碼。

什麼是基準?

基準測試只是與某些代碼的執行相關的一組測量或一組測量。基準測試允許您在開始努力提高性能時比較代碼的相對性能。基準測試的範圍可能非常廣泛,或者通常情況下您可能會發現自己測試微基準測試中的微小變化。主要的是確保您有一種機制來將建議的更改與原始代碼進行比較,然後指導您的優化工作。在優化代碼時使用數據非常重要,而不是假設。

如何基準C#代碼

希望到現在為止你已經按照基準測試的概念出售了,所以讓我們從一個簡單的例子開始。如果您想跟進,可以在此示例存儲庫的“基準”分支中找到此帖子的完整代碼。

讓我們假設我們已經將以下NameParser識別為我們在重負載和潛在性能瓶頸下的應用領域。

public class NameParser
{
public string GetLastName(string fullName)
{
var names = fullName.Split(" ");
var lastName = names.LastOrDefault();
return lastName ?? string.Empty;
}
}

此代碼是一個簡單的實現,用於從輸入字符串返回姓氏,該輸入字符串被假定為人的全名。出於本演示的目的,它假設最後一個單詞,在任何空格表示姓氏之後。現在這是一個非常簡單的示例,您可能想要進行基準測試的方法可能會做更復雜的工作!有時,您將能夠直接引用現有代碼庫中的代碼並對其進行基準測試,這些代碼庫的方法足夠小且公開。在其他時候,我發現自己通過將相關的代碼部分複製到我的基準項目中來創建基準,以便將焦點縮小到特定的代碼行。這是我需要花費更多時間的一個領域,以確定圍繞構建基準的良好實踐。

第一步是安裝Benchmark.NET庫。通常,您可能已經在進行單元測試,您將創建一個單獨的項目來保存您的基準測試。在此基準測試項目中,您將引用包含要進行基準測試的代碼的項目。為了使我的樣本非常簡單,我現在已將所有內容都保留在一個項目中。

對於一般基準測試,您只需要NuGet的主要BenchmarkDotNet軟件包。我通過從命令行使用“dotnet add package BenchmarkDotNet -version 0.11.3”將它添加到我的示例項目中來安裝我的。

下一步是通過創建一個包含它們的新類來創建基準。基準類將由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方法的性能和分配。

最後一步是設置並觸發Benchmark.NET的運行器。在這個示例中,我正在運行單個項目中的所有內容,因此我將更新Program類的Main方法。

public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<nameparserbenchmarks>();
}
}

/<nameparserbenchmarks>

對通用BenchmarkRunner.Run方法的調用接受應運行任何基準測試的類。默認情況下,基準測試的結果將記錄到控制檯。

執行基準

在這個階段,我們已準備好運行基準測試。為了獲得最佳效果,建議您在設備上執行此操作,儘可能少運行。關閉所有其他應用程序並殺死不必要的進程將產生最穩定的結果。在我的開發機器上,一旦關閉所有內容,我將觸發從命令行運行基準測試。

應根據發佈代碼運行基準測試,以確保包含所有優化。從我的項目目錄,我將運行“dotnet build -c Release”來創建發佈版本。

構建完成後,我可以導航到包含構建代碼的文件夾:“cd bin / Release / netcoreapp2.2”

最後,我可以通過使用“dotnet BenchmarkAndSpanExample.dll”為我的示例應用程序運行構建的程序集來運行基準測試

運行基準測試所需的時間長短取決於您的機器和測試代碼。Benchmark.NET執行許多階段來預熱代碼並確保運行多次迭代以提供一致的統計數據。它使用一個試驗階段計算出要運行的最佳迭代次數,儘管您可以根據需要進行配置。

解釋結果

完成後,您應該將摘要結果寫入控制檯窗口。如果您願意,可以在運行應用程序的位置下的BenchmarkDotNet.Artifacts文件夾中生成各種輸出。這包括摘要的HTML版本,可以更容易地共享。

我的機器的摘要如下所示:

// * Summary *

BenchmarkDotNet=v0.11.3, OS=Windows 10.0.16299.904 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
Frequency=3117195 Hz, Resolution=320.8012 ns, Timer=TSC
.NET Core SDK=3.0.100-preview-010184
[Host] : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT
DefaultJob : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT

Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
------------ |---------:|---------:|---------:|------:|------------:|------------:|------------:|--------------------:|
GetLastName | 125.8 ns | 2.306 ns | 2.157 ns | 1.00 | 0.0253 | - |

對於每個基準測試方法,您將獲得一行結果數據。在這裡,我有一行用於GetLastName方法的基準測試。它的平均執行時間是125.8納秒; 不是太寒酸!其他統計數據可用於迭代中的定時數據的誤差和標準偏差。

因為我包含了memory diagnoser屬性,所以我有一些包含內存相關統計信息的額外列。前三列與GC集合相關。它們被縮放以顯示每1,000次操作的數量。在這種情況下,我必須經常調用我的方法來觸發Gen 0集合,並且不太可能導致Gen 1或Gen 2集合。最後一列非常有用,它顯示了每個操作分配的內存。我的名稱解析器代碼每次調用時都會分配160字節。在宏偉的計劃中,這根本不多,但我們將在未來的帖子中看到我們如何減少這一點。請記住,儘管.NET中的分配很便宜,但GC工作可能會對收集和清理這些對象造成更大的影響。在熱門路徑(高度稱為方法)中,這很快就會增加。


分享到:


相關文章: