.NET Core中的性能測試工具BenchmarkDotnet

背景介紹

以前一篇博客中,咱們講解.NET Core中的CSV解析庫,在文章的最後,做者使用了性能基準測試工具BenchmarkDotNet測試了2個不一樣CSV解析庫的性能,本篇咱們來詳細介紹一下BenchmarkDotNet。html

原文連接:https://dotnetcoretutorials.com/2017/12/04/benchmarking-net-core-code-benchmarkdotnet/程序員

爲何須要性能基準測試?

性能基準測試能夠幫助程序員對比2個代碼段或者方法的性能,這對於代碼重寫或者重構來講,能夠提供一種很好的量化標準。若是沒有性能基準測試,很難想象將方法A改成B方法時候,僅憑肉眼如何區分性能的變化。app

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找到第一個知足條件的對象以後,不會中止查找,它會去繼續查找集合中的剩餘對象,直到遍歷整個集合或者在集合中找到第二個匹配條件的對象。 這裏咱們只是爲了演示一下如何進行代碼基準測試。spa

爲了使用BenchmarkDotNet來進行代碼基準測試,咱們首先建立一個空的.Net Core控制檯程序。調試

而後咱們使用Package Manage Console添加BenchmarkDotNet庫code

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萬對象的字符串集合。
  • 咱們在集合的中間位置插入了一個測試字符串,字符串的內容是"needle"。
  • 代碼中的<code>Single</code>和<code>First</code>方法,分別調用了Linq to object的<code>SingleOrDefault</code>和<code>FirstOrDefault</code>方法來查詢字符串集合中的"needle"字符串。
  • 在<code>Single</code>和<code>First</code>方法上,咱們加入<code>[Benchmark]</code>特性, 擁有該特性的方法會出如今最後的基準檢測報告中。

注意:

  • 測試的方法必須是公開的(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個方法處理的平均響應時間,<code>First</code>比<code>Single</code>快了一倍(這和咱們測試字符串放置的位置有關係)。

帶測試參數的基準測試(Input Benchmarking)

BenchmarkDotNet中咱們還可使用<code>[ParamsSource]</code>參數來指定測試的用例範圍。
在上面的代碼中,咱們測試了匹配字符串在集合中間位置時,<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();
        }
    }
}

代碼解釋說明

  • 咱們建立了測試的用例字符串集合<code>_needles</code>
  • 在構造函數中,咱們在字符串集合的頭部,中部,尾部分別插入了3個字符串
  • 咱們添加了一個屬性<code>Needle</code>, 表示當前測試的用例,在被測試<code>Single</code>和<code>First</code>方法中,咱們使用屬性<code>Needle</code>來匹配
  • 在屬性Needle上咱們加上了參數來源特性<code>[ParamsSource]</code>, 並設置參數來源是<code>_needles</code>

最終效果

如今咱們運行程序,程序產生的最終報告以下

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

從結果上看

  • 當匹配字符串在集合頭部的時候,<code>First</code>性能比<code>Single</code>高的多
  • 當匹配字符串在集合中部的時候,<code>First</code>性能是比<code>Single</code>的一倍
  • 當匹配字符串在集合尾部的時候,<code>First</code>和比<code>Single</code>的性能差很少

加入內存測試

.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個方法執行時的內存使用狀況。

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了麼?

本文源代碼

相關文章
相關標籤/搜索