|
|
|
|
|
將你的代碼包裝在計時器中并運行它幾萬次,這種做法并不完全可靠。你可能會陷入太多的陷阱,從而完全扭曲你的結(jié)果。在這種情況下,使用 BenchmarkDotNet 進行代碼基準測試是最好的選擇。 參閱文章:
代碼基準
代碼基準測試是指你想要將兩段代碼/方法相互比較。這是量化代碼重寫或重構(gòu)的好方法,它將成為 BenchmarkDotNet 最常見的用例。
首先,創(chuàng)建一個空白的 .NET Core 控制臺應用程序?,F(xiàn)在,大多數(shù)這些“應該”在使用 .NET Full Framework 時也有效,但我將在 .NET Core 中完成這里的所有操作。
接下來,你需要從包管理器控制臺運行以下命令來安裝 BenchmarkDotNet nuget 包:
Install-Package BenchmarkDotNet
接下來我們需要構(gòu)建我們的代碼。為此,我們將使用經(jīng)典的“大海撈針”。我們將在 C# 中構(gòu)建一個包含隨機項目的大型列表,并在列表中間放置一個“針”。然后我們將比較在列表上執(zhí)行“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();
}
}
}
稍微了解一下,首先我們創(chuàng)建一個類來保存我們的基準。這可以包含任意數(shù)量的私有方法,并且可以在構(gòu)造函數(shù)中包含設置代碼。構(gòu)造函數(shù)中的任何代碼都不包括在方法的計時中。然后我們可以創(chuàng)建公共方法并添加[Benchmark]的屬性, 將它們列為應該進行比較和基準測試的項目。
最后,在我們控制臺應用程序的主要方法中,我們使用“BenchmarkRunner
”類來運行我們的基準測試。
運行基準測試工具時的注意事項。它必須以“發(fā)布”模式構(gòu)建,并從命令行運行。你不應該使用從 Visual Studio 運行的基準測試,因為它還附加了一個調(diào)試器并且未編譯為“優(yōu)化”。要從命令行運行,請前往你的應用程序 bin/Release/netcoreappxx/ 文件夾,然后運行 ??dotnet {你的dll名}.dll
結(jié)果呢?
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 找到一個項目時,它仍然需要遍歷整個列表的其余部分,因為如果有多個,它需要拋出異常。當我們將項目放在列表中間時,這是有道理的!
輸入基準
假設我們發(fā)現(xiàn) Single 比 First 慢。我們有一個關于為什么會這樣的理論(那個 Single 需要繼續(xù)通過列表),然后我們可能需要一種方法來嘗試不同的“配置”,而不必在更改次要細節(jié)的情況下重新運行測試。為此,我們可以使用 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();
}
}
}
我們在這里所做的是創(chuàng)建一個“_needles”屬性來保存我們可能希望找到的不同針頭。我們已將它們插入列表中的不同索引處。然后,我們創(chuàng)建一個具有 ParamsSource 屬性的“Needle”屬性。這告訴 BenchmarkDotNet 循環(huán)遍歷這些值并為每個可能的值運行不同的測試。
一個重要的提示是 ParamsSource 必須是公共的。
運行這個,我們的報告現(xiàn)在看起來像這樣:
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 |
這有點難看,因為我們現(xiàn)在根據(jù)“First”返回 StartNeedle 所需的時間減少到納秒。但是結(jié)果很明顯。
運行 Single 時,返回針所需的時間是相同的,無論它在列表中的哪個位置。而 First 的響應時間完全取決于項目在列表中的位置。
輸入功能可以極大地幫助理解應用程序在給定不同輸入的情況下如何或為什么會變慢。例如,當密碼較長時,你的密碼散列函數(shù)會變慢嗎?或者它根本不是一個因素?
創(chuàng)建基線
最后一個有用的提示只是在報告上創(chuàng)建一個漂亮的小“乘數(shù)”,就是將你的基準之一標記為“基線”。如果我們回到我們的第一個例子(沒有輸入),我們只需要像這樣將我們的一個基準標記為基線: [Benchmark(Baseline = true)]
現(xiàn)在,當我們以標記為基線的“First”運行測試時,輸出現(xiàn)在如下所示:
Method | Mean | Scaled |
------- |---------:|-------:|
Single | 22.77 ms | 1.99 |
First | 11.42 ms | 1.00 |
所以現(xiàn)在更容易看出我們的其他方法變慢(或變快)的“因素”。在這種情況下,我們的 Single 調(diào)用幾乎是 First 調(diào)用的兩倍。
總結(jié)
作為程序員,我們喜歡看到微小的變化如何突飛猛進地提高性能,本文介紹了使用 BenchmarkDotNet 對代碼進行基準測試的方法,希望本文對你在對C#的代碼性能測試方面有所幫助。
相關文章