[实践]使用BenchmarkDotNet对.NET代码进行基准测试
作者:admin 时间:2023-5-13 10:12:25 浏览:将你的代码包装在计时器中并运行它几万次,这种做法并不完全可靠。你可能会陷入太多的陷阱,从而完全扭曲你的结果。在这种情况下,使用 BenchmarkDotNet 进行代码基准测试是最好的选择。 参阅文章:
代码基准
代码基准测试是指你想要将两段代码/方法相互比较。这是量化代码重写或重构的好方法,它将成为 BenchmarkDotNet 最常见的用例。
首先,创建一个空白的 .NET Core 控制台应用程序。现在,大多数这些“应该”在使用 .NET Full Framework 时也有效,但我将在 .NET Core 中完成这里的所有操作。
接下来,你需要从包管理器控制台运行以下命令来安装 BenchmarkDotNet nuget 包:
Install-Package BenchmarkDotNet
接下来我们需要构建我们的代码。为此,我们将使用经典的“大海捞针”。我们将在 C# 中构建一个包含随机项目的大型列表,并在列表中间放置一个“针”。然后我们将比较在列表上执行“SingleOrDefault”与“FirstOrDefault”的效果。这是我们的完整代码:
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;
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);
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<SingleVsFirst>();
Console.ReadLine();
}
}
}
稍微了解一下,首先我们创建一个类来保存我们的基准。这可以包含任意数量的私有方法,并且可以在构造函数中包含设置代码。构造函数中的任何代码都不包括在方法的计时中。然后我们可以创建公共方法并添加[Benchmark]的属性, 将它们列为应该进行比较和基准测试的项目。
最后,在我们控制台应用程序的主要方法中,我们使用“BenchmarkRunner
”类来运行我们的基准测试。
运行基准测试工具时的注意事项。它必须以“发布”模式构建,并从命令行运行。你不应该使用从 Visual Studio 运行的基准测试,因为它还附加了一个调试器并且未编译为“优化”。要从命令行运行,请前往你的应用程序 bin/Release/netcoreappxx/ 文件夹,然后运行 dotnet {你的dll名}.dll
结果呢?
Method | Mean | StdDev | Median |
------- |----------:|----------:|----------:|
Single | 15.591 ms | 0.4429 ms | 15.507 ms |
First | 7.638 ms | 0.4399 ms | 7.475 ms |
所以看起来 Single 比 First 慢两倍!如果你了解 Single 在幕后做了什么,这是可以预料的。当 First 找到一个项目时,它会立即返回(毕竟,它只想要“第一个”项目)。然而,当 Single 找到一个项目时,它仍然需要遍历整个列表的其余部分,因为如果有多个,它需要抛出异常。当我们将项目放在列表中间时,这是有道理的!
输入基准
假设我们发现 Single 比 First 慢。我们有一个关于为什么会这样的理论(那个 Single 需要继续通过列表),然后我们可能需要一种方法来尝试不同的“配置”,而不必在更改次要细节的情况下重新运行测试。为此,我们可以使用 BenchmarkDotNet 的“输入”功能。
让我们稍微修改一下我们的代码:
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”属性来保存我们可能希望找到的不同针头。我们已将它们插入列表中的不同索引处。然后,我们创建一个具有 ParamsSource 属性的“Needle”属性。这告诉 BenchmarkDotNet 循环遍历这些值并为每个可能的值运行不同的测试。
一个重要的提示是 ParamsSource 必须是公共的。
运行这个,我们的报告现在看起来像这样:
Method | Needle | Mean | StdDev |
------- |------------- |-----------------:|-----------------:|
Single | EndNeedle | 19,741,752.75 ns | 1,078,431.672 ns |
First | EndNeedle | 18,422,088.07 ns | 998,023.064 ns |
Single | MiddleNeedle | 19,326,424.98 ns | 1,356,796.153 ns |
First | MiddleNeedle | 9,586,518.55 ns | 649,534.186 ns |
Single | StartNeedle | 18,509,550.74 ns | 1,113,976.063 ns |
First | StartNeedle | 77.90 ns | 7.782 ns |
这有点难看,因为我们现在根据“First”返回 StartNeedle 所需的时间减少到纳秒。但是结果很明显。
运行 Single 时,返回针所需的时间是相同的,无论它在列表中的哪个位置。而 First 的响应时间完全取决于项目在列表中的位置。
输入功能可以极大地帮助理解应用程序在给定不同输入的情况下如何或为什么会变慢。例如,当密码较长时,你的密码散列函数会变慢吗?或者它根本不是一个因素?
创建基线
最后一个有用的提示只是在报告上创建一个漂亮的小“乘数”,就是将你的基准之一标记为“基线”。如果我们回到我们的第一个例子(没有输入),我们只需要像这样将我们的一个基准标记为基线: [Benchmark(Baseline = true)]
现在,当我们以标记为基线的“First”运行测试时,输出现在如下所示:
Method | Mean | Scaled |
------- |---------:|-------:|
Single | 22.77 ms | 1.99 |
First | 11.42 ms | 1.00 |
所以现在更容易看出我们的其他方法变慢(或变快)的“因素”。在这种情况下,我们的 Single 调用几乎是 First 调用的两倍。
总结
作为程序员,我们喜欢看到微小的变化如何突飞猛进地提高性能,本文介绍了使用 BenchmarkDotNet 对代码进行基准测试的方法,希望本文对你在对C#的代码性能测试方面有所帮助。
相关文章
- 站长推荐