背景介紹
之前一篇博客中留夜,我們講解.NET Core中的CSV解析庫真椿,在文章的最后浑劳,作者使用了性能基準(zhǔn)測試工具BenchmarkDotNet測試了2個不同CSV解析庫的性能秋泄,本篇我們來詳細介紹一下BenchmarkDotNet琐馆。
原文鏈接:https://dotnetcoretutorials.com/2017/12/04/benchmarking-net-core-code-benchmarkdotnet/
為什么需要性能基準(zhǔn)測試?
性能基準(zhǔn)測試可以幫助程序員對比2個代碼段或者方法的性能,這對于代碼重寫或者重構(gòu)來說恒序,可以提供一種很好的量化標(biāo)準(zhǔn)。如果沒有性能基準(zhǔn)測試谁撼,很難想象將方法A改為B方法時候歧胁,僅憑肉眼如何區(qū)分性能的變化。
BenchmarkDotNet
BenchmarkDotNet是一款強力的.NET性能基準(zhǔn)測試庫, 官網(wǎng)https://benchmarkdotnet.org/厉碟。
運行時支持
- NET Framework (4.6+),
- .NET Core (2.0+)
- Mono
- CoreRT喊巍。
BenchmarkDotnet為每個被測試的方法提供了孤立的環(huán)境, 使用BenchmarkDotnet, 程序員可以很容易的編寫各種性能測試方法,并可以避免許多常見的坑箍鼓。
代碼基準(zhǔn)測試(Code Benchmarking)
現(xiàn)在我們希望來對比一下Linq to object中First和Single方法的性能
雖然我們知道First的性能肯定比Single高, First方法會在查詢到第一個滿足條件的對象之后就停止集合遍歷崭参,而Single找到第一個滿足條件的對象之后,不會停止查找款咖,它會去繼續(xù)查找集合中的剩余對象何暮,直到遍歷整個集合或者在集合中找到第二個匹配條件的對象。 這里我們只是為了演示一下如何進行代碼基準(zhǔn)測試铐殃。
為了使用BenchmarkDotNet來進行代碼基準(zhǔn)測試海洼,我們首先創(chuàng)建一個空的.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();
}
}
代碼解釋說明
- 以上代碼中<code>SingleVsFirst</code>類是一個測試類富腊。
- 測試類中我們生成了一個擁有100萬對象的字符串集合坏逢。
- 我們在集合的中間位置插入了一個測試字符串,字符串的內(nèi)容是"needle"赘被。
- 代碼中的<code>Single</code>和<code>First</code>方法是整,分別調(diào)用了Linq to object的<code>SingleOrDefault</code>和<code>FirstOrDefault</code>方法來查詢字符串集合中的"needle"字符串。
- 在<code>Single</code>和<code>First</code>方法上民假,我們加入<code>[Benchmark]</code>特性, 擁有該特性的方法會出現(xiàn)在最后的基準(zhǔn)檢測報告中浮入。
注意:
- 測試的方法必須是公開的(public), 如果把public去掉,程序不會產(chǎn)生任何結(jié)果
- 在運行程序之前阳欲,還有一步關(guān)鍵的操作舵盈,測試的程序需要使用Release模式編譯陋率,并且不能附加任何調(diào)試器(Debugger)
最終結(jié)果
現(xiàn)在我們運行程序,程序產(chǎn)生的最終報告如下
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 |
結(jié)果中的第一列Mean表明了2個方法處理的平均響應(yīng)時間秽晚,<code>First</code>比<code>Single</code>快了一倍(這和我們測試字符串放置的位置有關(guān)系)瓦糟。
帶測試參數(shù)的基準(zhǔn)測試(Input Benchmarking)
BenchmarkDotNet中我們還可以使用<code>[ParamsSource]</code>參數(shù)來指定測試的用例范圍。
在上面的代碼中赴蝇,我們測試了匹配字符串在集合中間位置時菩浙,<code>First</code>和<code>Single</code>的效率對比,下面我們修改上面的代碼句伶,我們希望分別測試匹配字符串在集合頭部劲蜻,尾部以及中間位置時<code>First</code>和<code>Single</code>的效率對比。
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();
}
}
}
代碼解釋說明
- 我們創(chuàng)建了測試的用例字符串集合<code>_needles</code>
- 在構(gòu)造函數(shù)中考余,我們在字符串集合的頭部先嬉,中部,尾部分別插入了3個字符串
- 我們添加了一個屬性<code>Needle</code>, 表示當(dāng)前測試的用例楚堤,在被測試<code>Single</code>和<code>First</code>方法中疫蔓,我們使用屬性<code>Needle</code>來匹配
- 在屬性Needle上我們加上了參數(shù)來源特性<code>[ParamsSource]</code>, 并設(shè)置參數(shù)來源是<code>_needles</code>
最終效果
現(xiàn)在我們運行程序,程序產(chǎn)生的最終報告如下
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
從結(jié)果上看
- 當(dāng)匹配字符串在集合頭部的時候身冬,<code>First</code>性能比<code>Single</code>高的多
- 當(dāng)匹配字符串在集合中部的時候衅胀,<code>First</code>性能是比<code>Single</code>的一倍
- 當(dāng)匹配字符串在集合尾部的時候,<code>First</code>和比<code>Single</code>的性能差不多
加入內(nèi)存測試
在.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]特性滚躯,我們還在測試類<code>CsvBenchmarking</code>上添加了<code>[MemoryDiagnoser]</code>特性,該特性會在測試報告中追加嘿歌,2個方法執(zhí)行時的內(nèi)存使用情況掸掏。
Method | Mean | Scaled | Allocated |
-------------- |-----------:|-------:|----------:|
CSVHelper | 1,404.5 ms | 1.00 | 244.39 MB |
TinyCsvParser | 381.6 ms | 0.27 | 32.53 MB |
其中Allocated表明了內(nèi)存占用情況。
總結(jié)
BenchmarkDotNet絕對是.NET開發(fā)人員了解代碼性能搅幅,以及對比代碼性能的必備神器阅束。你的項目里用了BenchmarkDotnet了么?