性能测试工具BenchmarkDotnet

.NET Core中的性能测试工具BenchmarkDotnet

https://www.cnblogs.com/lwqlun/p/9671611.html

背景介绍
之前一篇博客中,我们讲解.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 _haystack = new List ();
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了么?

本文源代码

猜你喜欢

转载自www.cnblogs.com/Leo_wl/p/9700786.html