C# 標準性能測試高級用法

本文告訴大家如何在項目使用性能測試測試自己寫的方法

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);

在運行的時候就可以選運行哪個


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章