本文告訴大家如何在項目使用性能測試測試自己寫的方法
在 C# 標準性能測試 已經告訴大家如何使用 BenchmarkDotNet 測試性能,本文會告訴大家高級的用法。
建議是創建一個控制檯項目用來做性能測試,這個項目要求是 dotnet framework 4.6 以上,建議是 4.7 的版本。使用這個項目引用需要測試的項目,然後在裏面寫測試的代碼。
例如被測試項目有一個類 Foo 裏面有一個方法是 lindexidb ,需要測試 林德熙逗比 方法的性能
最簡單的測試的代碼
public class FooPerf { [Benchmark] public void lindexidb() { new Foo().lindexidb(); } }
在 Main 函數使用下面代碼
var boKar = BenchmarkRunner.Run<Foo>();
這樣就可以進行測試,如果需要傳入一些參數,那麼就需要使用本文的方法
傳入參數
如果需要測試的方法需要傳入不同的參數,而且在使用不同的參數的性能也是不相同,就需要使用傳入參數特性。
例如有底層的項目
public class Foo { public void Lindexidb() { } }
需要創建另一個項目測試這個項目的性能, 需要注意不要在自己的庫安裝 BenchmarkDotNet ,安裝之後會讓啓動速度慢很多
在測試性能的另一個項目,安裝 BenchmarkDotNet 引用庫測試,所有的代碼
class Program { static void Main(string[] args) { BenchmarkRunner.Run<FooPerf>(); Console.Read(); } } public class FooPerf { [Benchmark] public void Lindexidb() { var foo = new Foo(); foo.Lindexidb(); } }
需要知道,必須設置 FooPerf 的訪問是 public 沒有設置會出現異常
現在例如修改了 Lindexidb 需要傳入參數
public class Foo { public void Lindexidb(int a, int b) { var foo = a + b; if (Arguments>a) { return; } if (foo == 2) { Arguments = foo; } } public int Arguments { get; set; } }
現在需要修改性能項目
public class FooPerf { [Benchmark(Description = "這裏可以寫這個方法是什麼")] public void Lindexidb() { var foo = new Foo(); foo.Lindexidb(2, 3); } }
可以看到上面寫法很難寫出測試很多參數
public class FooPerf { [Benchmark] [Arguments(100, 10)] [Arguments(2, 10)] [Arguments(2, 3)] [Arguments(10, 3)] [Arguments(21, 3)] public void Lindexidb(int a,int b) { var foo = new Foo(); foo.Lindexidb(a, b); } }
通過 Arguments 可以傳入不同的參數,使用這個方法可以防止初始化參數需要的時間計算爲算法的
運行程序可以看到下面輸出
// FooPerf.Lindexidb: DefaultJob [a=2, b=3] // FooPerf.Lindexidb: DefaultJob [a=2, b=10] // FooPerf.Lindexidb: DefaultJob [a=10, b=3] // FooPerf.Lindexidb: DefaultJob [a=21, b=3] // FooPerf.Lindexidb: DefaultJob [a=100, b=10]
在使用不同的參數可以看到不同的速度
Method | a | b | Mean | Error | StdDev |
---|---|---|---|---|---|
Lindexidb | 2 | 3 | 2.037 ns | 0.0749 ns | 0.0833 ns |
Lindexidb | 2 | 10 | 3.263 ns | 0.0992 ns | 0.2682 ns |
Lindexidb | 10 | 3 | 2.333 ns | 0.0798 ns | 0.1038 ns |
Lindexidb | 21 | 3 | 2.278 ns | 0.0776 ns | 0.0863 ns |
Lindexidb | 100 | 10 | 2.364 ns | 0.0809 ns | 0.2242 ns |
可以傳入不同的參數,傳入的參數可以自動轉換
如果傳入的參數不對,就會提示,如下面代碼
[Benchmark] [Arguments("123", "123")] [Arguments(2, 10)] [Arguments(2, 3)] [Arguments(10, 3)] [Arguments(21, 3)] public void Lindexidb(int a,int b) { var foo = new Foo(); foo.Lindexidb(a, b); }
本來是使用 int 但是參數寫 string 所以會出現下面提示
// Build Exception: The build has failed! CS0029: Cannot implicitly convert type 'string' to 'int' CS0029: Cannot implicitly convert type 'string' to 'int'
如果需要參數是 一個,如代碼,傳入的參數是兩個,那麼會出現異常
public class FooPerf { [Benchmark] [Arguments(1, 2)] [Arguments(2, 10)] [Arguments(2, 3)] [Arguments(10, 3)] [Arguments(21, 3)] public void Lindexidb(int a) { var foo = new Foo(); var b = Arguments; foo.Lindexidb(a, b); } public int Arguments { get; set; } }
屬性
屬性和字段都可以修改,但是修改字段需要修改公開字段,不推薦修改字段
[Params(10, 2, 3)] public int Arguments { get; set; }
可以設置屬性的值爲 10,2,3
在下面代碼會組合屬性和傳入參數
[Benchmark(Description = "這裏可以寫這個方法是什麼")] [Arguments(1)] [Arguments(2)] [Arguments(2)] [Arguments(10)] [Arguments(21)] public void Lindexidb(int a) { var foo = new Foo(); var b = Arguments; foo.Lindexidb(a, b); } [Params(10, 2, 3)] public int Arguments { get; set; }
運行看到有 15 個測試
// FooPerf.Lindexidb: DefaultJob [Arguments=2, a=1] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=2] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=2] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=10] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=21] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=1] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=2] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=2] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=10] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=21] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=1] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=2] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=2] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=10] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=21]
傳入多個值
可以看到在特性寫參數是比較多的,如果需要很多參數就需要寫很多代碼
可以使用數組的方式把很多的代碼作爲數組
請看代碼
[Benchmark(Description = "這裏可以寫這個方法是什麼")] [ArgumentsSource(nameof(LeesikeasowSearjeeball))] public void Lindexidb(int a, int b) { var foo = new Foo(); foo.Lindexidb(a, b); } public IEnumerable<object[]> LeesikeasowSearjeeball() { yield return new object[] {2, 3}; yield return new object[] {10, 2}; yield return new object[] {5, 2}; yield return new object[] {100, 5}; yield return new object[] {3, 100}; }
上面使用 LeesikeasowSearjeeball 作爲輸入的參數,注意需要返回一個數組,這個數組裏就是參數的列表。上面使用的參數有兩個,所以數組就是包含兩個參數
// FooPerf.Lindexidb: DefaultJob [Arguments=2, a=2, b=3] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=3, b=100] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=5, b=2] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=10, b=2] // FooPerf.Lindexidb: DefaultJob [Arguments=2, a=100, b=5] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=2, b=3] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=3, b=100] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=5, b=2] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=10, b=2] // FooPerf.Lindexidb: DefaultJob [Arguments=3, a=100, b=5] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=2, b=3] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=3, b=100] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=5, b=2] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=10, b=2] // FooPerf.Lindexidb: DefaultJob [Arguments=10, a=100, b=5]
除了可以設置方法傳入,還可以設置屬性
[Benchmark] public void Foo() { for (int i = 0; i < Arguments; i++) { } } [ParamsSource(nameof(PememasiDismikasu))] public int Arguments { get; set; } public IEnumerable<int> PememasiDismikasu => new[] { 100, 200 };
通過 ParamsSource 可以告訴測試使用的從哪個拿到
基線
基線可以用在三個不同的地方,最簡單的是方法,另外可以用在分類和不同環境。
因爲測試的時間在不同的設備的時間都不相同,如何判斷一個方法優化之後是比原來好?方法就是把原來的方法作爲基線,這樣可以對比不同的方法的速度
如有三個不同的方法,選一個作爲基線
[Benchmark] public void Time50() => Thread.Sleep(50); [Benchmark(Baseline = true)] public void Time100() => Thread.Sleep(100); [Benchmark] public void Time150() => Thread.Sleep(150);
設置基線的方法是添加 Baseline = true ,建議在原來的方法添加,然後使用不同的方法看哪個方法的速度比較快
在輸出會添加一列 Scaled
用於表示這個方法對比基線的速度,他的時間是基線的多少。如上面代碼的運行會輸出
Method | Mean | Error | StdDev | Scaled |
---|---|---|---|---|
Time50 | 50.46 ms | 0.0779 ms | 0.0729 ms | 0.50 |
Time100 | 100.39 ms | 0.0762 ms | 0.0713 ms | 1.00 |
Time150 | 150.48 ms | 0.0986 ms | 0.0922 ms | 1.50 |
這裏的 Scaled 就是對比基線方法的時間
如果在不同的分類下需要做不同的標準,就可以在 BenchmarkCategory 添加 Baseline 告訴在哪個分類使用哪個方法作爲標準。如下面的代碼,設置 Fast 類和 Slow 類使用不同的標準
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [CategoriesColumn] public class IntroCategoryBaseline { [BenchmarkCategory("Fast"), Benchmark(Baseline = true)] public void Time50() => Thread.Sleep(50); [BenchmarkCategory("Fast"), Benchmark] public void Time100() => Thread.Sleep(100); [BenchmarkCategory("Slow"), Benchmark(Baseline = true)] public void Time550() => Thread.Sleep(550); [BenchmarkCategory("Slow"), Benchmark] public void Time600() => Thread.Sleep(600); }
運行的輸出,可以看到對於不同的分類用的是不同的方法
Method | Categories | Mean | Error | StdDev | Scaled |
---|---|---|---|---|---|
Time50 | Fast | 50.46 ms | 0.0745 ms | 0.0697 ms | 1.00 |
Time100 | Fast | 100.47 ms | 0.0955 ms | 0.0893 ms | 1.99 |
Time550 | Slow | 550.48 ms | 0.0525 ms | 0.0492 ms | 1.00 |
Time600 | Slow | 600.45 ms | 0.0396 ms | 0.0331 ms | 1.09 |
基線除了可以測試方法的基線,還可以測試環境。如我的代碼需要在 Clr
Mono
Core
三個不同環境運行,這時我想知道對比 Clr 環境,其他兩個環境的性能。可以使用 JobBaseline 的方式。
[ClrJob(baseline: true)] [MonoJob] [CoreJob] public class IntroJobBaseline { [Benchmark] public int SplitJoin() => string.Join(",", new string[1000]).Split(',').Length; }
這時輸出可以看到 Clr 運行的是標準,在 Core 運行時間是在 Clr 運行的 0.67 通過這個方法就知道在不同的環境相同的方法的測試
Method | Runtime | Mean | Error | StdDev | Scaled | ScaledSD |
---|---|---|---|---|---|---|
SplitJoin | Clr | 19.42 us | 0.2447 us | 0.1910 us | 1.00 | 0.00 |
SplitJoin | Core | 13.00 us | 0.2183 us | 0.1935 us | 0.67 | 0.01 |
SplitJoin | Mono | 39.14 us | 0.7763 us | 1.3596 us | 2.02 | 0.07 |
更多關於基線請看 Benchmark and Job Baselines
分類
如果在一個類的測試方法有不同的類型,而只需要測試某幾個類型的就需要使用本文的方法
[DryJob] [CategoriesColumn] [BenchmarkCategory("分類")] [AnyCategoriesFilter("A", "1")] public class FooPerf { [Benchmark] [BenchmarkCategory("A", "1")] public void A1() => Thread.Sleep(10); // Will be benchmarked [Benchmark] [BenchmarkCategory("A", "2")] public void A2() => Thread.Sleep(10); // Will be benchmarked [Benchmark] [BenchmarkCategory("B", "1")] public void B1() => Thread.Sleep(10); // Will be benchmarked [Benchmark] [BenchmarkCategory("B", "2")] public void B2() => Thread.Sleep(10); }
在方法表示方法屬於的類型,可以標記一個方法屬於多個類型,如 A1 方法屬於 A
1
兩個類型,在類標記 AnyCategoriesFilter 表示測試某些類型,這裏標記了 A
和 1 也就是所有包含 A 或 1 類型的方法會被測試,所以 A1 A2 B1 都會被運行。
包含名字
如果在一個類有很多方法,只需要名字滿足某些條件的方法纔可以執行,就需要進行包含名字判斷。
[Config(typeof(Config))] public class FooPerf { private class Config : ManualConfig { // 只有在名字滿足包含 "A" 或 "1" 和名字長度小於 3 才執行 public Config() { Add(new DisjunctionFilter ( // 這裏的是或關係 // 只要名字包含 "A" 或 "1" 就執行 new NameFilter(name => name.Contains("A")), new NameFilter(name => name.Contains("1")) )); // 這裏和上面是 And 關係,也就是必須要同時滿足名字長度小於 3 纔可以執行 Add(new NameFilter(name => name.Length < 3)); } } [Benchmark] public void A1() => Thread.Sleep(10); // Will be benchmarked [Benchmark] public void A2() => Thread.Sleep(10); // Will be benchmarked [Benchmark] public void A3() => Thread.Sleep(10); // Will be benchmarked [Benchmark] public void B1() => Thread.Sleep(10); // Will be benchmarked [Benchmark] public void B2() => Thread.Sleep(10); [Benchmark] public void B3() => Thread.Sleep(10); [Benchmark] public void C1() => Thread.Sleep(10); // Will be benchmarked [Benchmark] public void C2() => Thread.Sleep(10); [Benchmark] public void C3() => Thread.Sleep(10); [Benchmark] public void Aaa() => Thread.Sleep(10); }
在類添加特性,告訴這個類需要使用哪個配置,在配置的構造函數使用了兩次的 Add 函數,在多個 Add 之間是 And 關係,也就是必須所有 Add 的條件都滿足纔可以執行。在一個Add使用的 DisjunctionFilter 可以使用或關係多個條件。
上面的函數使用的滿足名字帶有 A 或 1 而且名字的長度小於 3 纔可以執行。
除了使用名字作爲條件,還可以使用 AnyCategoriesFilter 表示存在任意的類型符合,AllCategoriesFilter 要求所有的類型都符合。
運行多個類
一個需要測試的類需要使用下面代碼
BenchmarkRunner.Run<FooPerf>();
只能測試一個類,如果有很多類就需要寫很多代碼,下面告訴大家如何找到所有方法
BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args);
在運行的時候就可以選運行哪個