.NET Core中的效能測試工具BenchmarkDotnet
背景介紹
之前一篇部落格中,我們講解 ofollow,noindex" target="_blank">.NET Core中的CSV解析庫 ,在文章的最後,作者使用了效能基準測試工具BenchmarkDotNet測試了2個不同CSV解析庫的效能,本篇我們來詳細介紹一下BenchmarkDotNet。
原文連結: https://dotnetcoretutorials.com/2017/12/04/benchmarking-net-core-code-benchmarkdotnet/
為什麼需要效能基準測試?
效能基準測試可以幫助程式員對比2個程式碼段或者方法的效能,這對於程式碼重寫或者重構來說,可以提供一種很好的量化標準。如果沒有效能基準測試,很難想象將方法A改為B方法時候,僅憑肉眼如何區分效能的變化。
BenchmarkDotNet
BenchmarkDotNet是一款強力的.NET效能基準測試庫, 官網https://benchmarkdotnet.org/。
執行時支援
- NET Framework (4.6+),
- .NET Core (2.0+)
- Mono
- CoreRT。
BenchmarkDotnet為每個被測試的方法提供了孤立的環境, 使用BenchmarkDotnet, 程式設計師可以很容易的編寫各種效能測試方法,並可以避免許多常見的坑。
程式碼基準測試(Code Benchmarking)
現在我們希望來對比一下Linq to object中First和Single方法的效能
雖然我們知道First的效能肯定比Single高, First方法會在查詢到第一個滿足條件的物件之後就停止集合遍歷,而Single找到第一個滿足條件的物件之後,不會停止查詢,它會去繼續查詢集合中的剩餘物件,直到遍歷整個集合或者在集合中找到第二個匹配條件的物件。 這裡我們只是為了演示一下如何進行程式碼基準測試。
為了使用BenchmarkDotNet來進行程式碼基準測試,我們首先建立一個空的.Net Core控制檯程式。
然後我們使用Package Manage Console新增BenchmarkDotNet庫
PM> Install-Package BenchmarkDotNet
然後我們修改Program.cs檔案, 程式碼如下
public class Program { public class SingleVsFirst { private readonly List<string> _haystack = new List<string>(); private readonly int _haystackSize = 1000000; private readonly string _needle = "needle"; public SingleVsFirst() { //Add a large amount of items to our list. Enumerable.Range(1, _haystackSize).ToList().ForEach(x => _haystack.Add(x.ToString())); //Insert the needle right in the middle. _haystack.Insert(_haystackSize / 2, _needle); } [Benchmark] public string Single() => _haystack.SingleOrDefault(x => x == _needle); [Benchmark] public string First() => _haystack.FirstOrDefault(x => x == _needle); } public static void Main(string[] args) { var summary = BenchmarkRunner.Run<SingleVsFirst>(); Console.ReadLine(); } }
程式碼解釋說明
- 以上程式碼中
SingleVsFirst
類是一個測試類。 - 測試類中我們生成了一個擁有100萬物件的字串集合。
- 我們在集合的中間位置插入了一個測試字串,字串的內容是"needle"。
- 程式碼中的
Single
和First
方法,分別呼叫了Linq to object的SingleOrDefault
和FirstOrDefault
方法來查詢字串集合中的"needle"字串。 - 在
Single
和First
方法上,我們加入[Benchmark]
特性, 擁有該特性的方法會出現在最後的基準檢測報告中。
注意:
- 測試的方法必須是公開的(public), 如果把public去掉,程式不會產生任何結果
- 在執行程式之前,還有一步關鍵的操作,測試的程式需要使用Release模式編譯,並且不能附加任何偵錯程式(Debugger)
最終結果
現在我們執行程式,程式產生的最終報告如下
Method|Mean |Error |StdDev |Median | ------- |---------:|----------:|---------:|---------:| Single | 28.12 ms | 0.9347 ms | 2.697 ms | 28.93 ms | First | 13.30 ms | 0.8394 ms | 2.475 ms | 14.48 ms |
結果中的第一列Mean表明了2個方法處理的平均響應時間, First
比 Single
快了一倍(這和我們測試字串放置的位置有關係)。
帶測試引數的基準測試(Input Benchmarking)
BenchmarkDotNet中我們還可以使用 [ParamsSource]
引數來指定測試的用例範圍。
在上面的程式碼中,我們測試了匹配字串在集合中間位置時, First
和 Single
的效率對比,下面我們修改上面的程式碼,我們希望分別測試匹配字串在集合頭部,尾部以及中間位置時 First
和 Single
的效率對比。
using System; using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; namespace BenchmarkExample { public class SingleVsFirst { private readonly List<string> _haystack = new List<string>(); private readonly int _haystackSize = 1000000; public List<string> _needles => new List<string> { "StartNeedle", "MiddleNeedle", "EndNeedle" }; public SingleVsFirst() { //Add a large amount of items to our list. Enumerable.Range(1, _haystackSize).ToList().ForEach(x => _haystack.Add(x.ToString())); //One at the start. _haystack.Insert(0, _needles[0]); //One right in the middle. _haystack.Insert(_haystackSize / 2, _needles[1]); //One at the end. _haystack.Insert(_haystack.Count - 1, _needles[2]); } [ParamsSource(nameof(_needles))] public string Needle { get; set; } [Benchmark] public string Single() => _haystack.SingleOrDefault(x => x == Needle); [Benchmark] public string First() => _haystack.FirstOrDefault(x => x == Needle); } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<SingleVsFirst>(); Console.ReadLine(); } } }
程式碼解釋說明
- 我們建立了測試的用例字串集合
_needles
- 在建構函式中,我們在字串集合的頭部,中部,尾部分別插入了3個字串
- 我們添加了一個屬性
Needle
, 表示當前測試的用例,在被測試Single
和First
方法中,我們使用屬性Needle
來匹配 - 在屬性Needle上我們加上了引數來源特性
[ParamsSource]
, 並設定引數來源是_needles
最終效果
現在我們執行程式,程式產生的最終報告如下
Method |Needle |Mean |Error |StdDev |Median | ------- |------------- |-----------------:|---------------:|-----------------:|-----------------:| Single |EndNeedle | 23,266,757.53 ns | 432,206.593 ns |591,609.263 ns | 23,236,343.07 ns | First |EndNeedle | 24,984,621.12 ns | 494,223.345 ns |783,890.599 ns | 24,936,945.21 ns | Single | MiddleNeedle | 21,379,814.14 ns | 806,253.579 ns | 2,377,256.870 ns | 22,436,101.14 ns | First | MiddleNeedle | 11,984,519.09 ns | 315,184.021 ns |924,380.173 ns | 12,233,700.94 ns | Single |StartNeedle | 23,650,243.23 ns | 599,968.173 ns |714,219.431 ns | 23,555,402.19 ns | First |StartNeedle |89.17 ns |1.864 ns |2.732 ns |89.07 ns
從結果上看
- 當匹配字串在集合頭部的時候,
First
效能比Single
高的多 - 當匹配字串在集合中部的時候,
First
效能是比Single
的一倍 - 當匹配字串在集合尾部的時候,
First
和比Single
的效能差不多
加入記憶體測試
在 .NET Core中的CSV解析庫 中,我們使用了以下程式碼
[MemoryDiagnoser] public class CsvBenchmarking { [Benchmark(Baseline =true)] public IEnumerable<Automobile> CSVHelper() { TextReader reader = new StreamReader("import.txt"); var csvReader = new CsvReader(reader); var records = csvReader.GetRecords<Automobile>(); return records.ToList(); } [Benchmark] public IEnumerable<Automobile> TinyCsvParser() { CsvParserOptions csvParserOptions = new CsvParserOptions(true, ','); var csvParser = new CsvParser<Automobile>(csvParserOptions, new CsvAutomobileMapping()); var records = csvParser.ReadFromFile("import.txt", Encoding.UTF8); return records.Select(x => x.Result).ToList(); } }
其中除了[Benchmark]特性,我們還在測試類 CsvBenchmarking
上添加了 [MemoryDiagnoser]
特性,該特性會在測試報告中追加,2個方法執行時的記憶體使用情況。
Method |Mean | Scaled | Allocated | -------------- |-----------:|-------:|----------:| CSVHelper | 1,404.5 ms |1.00 | 244.39 MB | TinyCsvParser |381.6 ms |0.27 |32.53 MB |
其中Allocated表明了記憶體佔用情況。
總結
BenchmarkDotNet絕對是.NET開發人員瞭解程式碼效能,以及對比程式碼效能的必備神器。你的專案裡用了BenchmarkDotnet了麼?