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