【.NET/C#】BenchmarkDotNetを使って、メソッドのパフォーマンスを簡単に集計する

# 概要
BenchmarkDotNetの紹介と使い方を掲載する。

BenchmarkDotNetはメソッドのパフォーマンスを簡単に計測できるライブラリです。
統計的な情報をレポートしてくれるため、関数のパフォーマンスを調査する業務で役立ちそう。

# 目次

* [BenchmarkDotNetとは？](#BenchmarkDotNetとは？)
* [設定手順](#設定手順)
* [参考にしたサイト](#参考にしたサイト)

# BenchmarkDotNetとは？

>BenchmarkDotNetは、メソッドをベンチマークに変換し、そのパフォーマンスを追跡し、
>再現性のある測定実験を共有するのに役立ちます。

>上記は[GitHub](https://github.com/dotnet/BenchmarkDotNet)の記載内容の機械翻訳

サポート対象は下記。.NET系なら大体使えそう。
本記事ではC#で実装する。
> Projects: classic and modern with PackageReferences
> Runtimes: Full .NET Framework (4.6+), .NET Core (2.0+), Mono, CoreRT
> OS: Windows, Linux, MacOS
> Languages: C#, F#, VB

[エクスポート機能](https://benchmarkdotnet.org/articles/configs/exporters.html)も充実している。

# 設定手順
下記ドキュメントのOverViewに従って、パフォーマンス計測を行う。
[BenchmarkDotNetの公式ドキュメント](https://benchmarkdotnet.org/articles/overview.html)

## 1.NuGetパッケージをインストール

Visual Studioを立ち上げ、「コンソールアプリケーション」を選択する。
下記のNuGetパッケージを追加する。
[NuGetリンク](https://www.nuget.org/packages/BenchmarkDotNet/)
![Nuget-Benchmark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/f3573b6e-9278-1b3c-02bb-d44363171e1a.png)

## 2.計測対象のメソッドを含むクラスを作成する
### 扱う題材(計測対象)

stringとStringBuilderの文字列結合の処理時間を比較する。
期待結果は「StringBuilderの方がパフォーマンスが優れる」こと。
※下記サイトで既に検証済み
[文字列処理を高速に行う](https://dobon.net/vb/dotnet/string/stringbuilder.html)

<details><summary>StringBuilderがパフォーマンスが優れる理由</summary>
下記の違いにより、扱う文字の数が増えるとStringBuilderの方がパフォーマンスに優れることが多い。
・string型は編集不可なため、文字列操作の度に新たなインスタンスを作る。
・StringBuilderは編集可能であるため、文字列操作で新たなインスタンスを作らない。
<a href="https://www.atmarkit.co.jp/fdotnet/vblab/vbroom_02/vbroom_02_02.html">stringとStringBuilderの違い</a>
</details>

### 計測対象のクラスを定義する
計測対象のメソッド含むクラスを定義する。
対象メソッドには[Benchmark]属性を指定する。

~~~C#
using BenchmarkDotNet.Attributes;
using System.Text;

public class StringConcatMesurement
{
    private int NumberOfItems = 200000;
    [Benchmark]
    public string WithStringBuilder()
    {
        var sb = new StringBuilder();
        for (int i = 0; i < NumberOfItems; i++)
        {
            sb.Append("1");
        }
        return sb.ToString();
    }
    [Benchmark]
    public string WithStringType()
    {
        string s = string.Empty;
        for (int i = 0; i < NumberOfItems; i++)
        {
            s += "1";
        }
        return s;
    }
}
~~~

### メイン関数からコールする

~~~C#
using BenchmarkDotNet.Running;

class Program
{
    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<StringConcatMesurement>();
    }
}
~~~

これで準備完了！！実行する前に一点だけ注意事項。
<b>[リリースビルド]で実行すること。
[デバックビルド]の場合は、下記のように実行時エラーになる。</b>
![BenchmarkDotNet-デバックモードはエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/aaf849cd-eabb-760e-24ee-0f84134a640c.png)

ちなみに、このままの状態で実行すると私の環境で10分以上掛かった。
ベンチマーク計測に正確さが必要ない場合、[ShortRunJob]属性をつけて下さい(後述の項 [計測時間の短縮をする方法](#計測時間の短縮をする方法)を参照)。

### 計測開始時
![BenchmarkDotNet開始時画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/9320e42d-0e19-9da1-2e67-8ebee0a24e22.png)
### 計測終了時
![BenchmarkDotNet終了時の画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/912970b0-6ded-404b-80e0-0eeaf0a9c8b6.png)

### 計測結果を確認する
下記の結果がコンソールに表示される。
・実行環境
・統計情報の表(関数名、平均値、エラー、標準偏差）
・統計情報のラベルの説明

~~~
// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.867 (2004/?/20H1)
Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.402
  [Host]     : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT
  DefaultJob : .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT


|            Method |           Mean |         Error |        StdDev |
|------------------ |---------------:|--------------:|--------------:|
| WithStringBuilder |       895.1 us |      15.62 us |      21.90 us |
|    WithStringType | 7,806,015.2 us | 155,282.10 us | 347,310.67 us |

// * Hints *
Outliers
  StringConcatMesurement.WithStringBuilder: Default -> 4 outliers were removed (978.11 us..1.02 ms)
  StringConcatMesurement.WithStringType: Default    -> 4 outliers were removed (9.74 s..10.10 s)

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 us   : 1 Microsecond (0.000001 sec)
~~~

予想通りStringBuilderの方がパフォーマンスが優れる。
平均値で比較すると、約8721倍StringBuilderが速い。
標準偏差を見ても、stringに比べると計測結果のバラツキが小さいことが分かる。

<b>余談</b>
コンソール出力内容はログに記録されている。
ソリューションのRelease直下にBenchmarkDotNet.Artifactsフォルダ内部に下記のログファイルが存在する。
「ソリューション名.計測対象クラス名-(日時:YYYYYMMDDD-HHMMS).log」

### 計測結果サマリをカスタマイズする
サマリーは下記の詳細結果のうち、属性でどれを出すかを指定する。
デフォルトだと、平均値、エラー、標準偏差を表示する。
試しに最小値、最大値を追加する。

~~~
// * Detailed results *
StringConcatMesurement.WithStringBuilder: DefaultJob
Runtime = .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT; GC = Concurrent Workstation
Mean = 830.978 us, StdErr = 3.726 us (0.45%), N = 15, StdDev = 14.432 us
Min = 809.898 us, Q1 = 821.067 us, Median = 826.252 us, Q3 = 844.369 us, Max = 855.115 us
IQR = 23.303 us, LowerFence = 786.113 us, UpperFence = 879.323 us
ConfidenceInterval = [815.549 us; 846.406 us] (CI 99.9%), Margin = 15.428 us (1.86% of Mean)
Skewness = 0.28, Kurtosis = 1.64, MValue = 2
-------------------- Histogram --------------------
[802.218 us ; 834.536 us) | @@@@@@@@@@
[834.536 us ; 857.584 us) | @@@@@
---------------------------------------------------

StringConcatMesurement.WithStringType: DefaultJob
Runtime = .NET Core 3.1.8 (CoreCLR 4.700.20.41105, CoreFX 4.700.20.41903), X64 RyuJIT; GC = Concurrent Workstation
Mean = 7.206 s, StdErr = 0.051 s (0.70%), N = 95, StdDev = 0.493 s
Min = 6.459 s, Q1 = 6.804 s, Median = 7.101 s, Q3 = 7.443 s, Max = 8.521 s
IQR = 0.639 s, LowerFence = 5.846 s, UpperFence = 8.402 s
ConfidenceInterval = [7.034 s; 7.378 s] (CI 99.9%), Margin = 0.172 s (2.38% of Mean)
Skewness = 0.84, Kurtosis = 3.15, MValue = 2.93
-------------------- Histogram --------------------
[6.317 s ; 6.608 s) | @@@@
[6.608 s ; 6.891 s) | @@@@@@@@@@@@@@@@@@@@@@@@
[6.891 s ; 7.243 s) | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
[7.243 s ; 7.605 s) | @@@@@@@@@@@@@@@@@@@
[7.605 s ; 7.892 s) | @@@@@@@
[7.892 s ; 8.269 s) | @@@@@@@
[8.269 s ; 8.663 s) | @@@
---------------------------------------------------
~~~

#### サマリーのカスタマイズ実装

~~~C#

[MinColumn,MaxColumn]//←新規追加
public class StringConcatMesurement
{
    private int NumberOfItems = 200000;
    [Benchmark]
    public string WithStringBuilder()
    {
    //略
    }
    [Benchmark]
    public string WithStringType()
    {
     //略
    }
}
~~~

#### カスタマイズ後のサマリー
最小値、最大値の項目が増えた。

~~~
|            Method |           Mean |         Error |        StdDev |            Min |            Max |
|------------------ |---------------:|--------------:|--------------:|---------------:|---------------:|
| WithStringBuilder |       831.0 us |      15.43 us |      14.43 us |       809.9 us |       855.1 us |
|    WithStringType | 7,205,897.0 us | 171,677.49 us | 492,574.91 us | 6,458,541.9 us | 8,520,846.9 us |
~~~

# 計測時間の短縮をする方法
関数のパフォーマンスを改善する場面などでは、正確性よりも試行回数が重視されると思う。
何も制限なしで使うと、文字列の結合の評価だけで約10分かかった。
とても待ってられない。
下記[ShortRunJob]属性指定することで、関数の試行回数(N=3)で固定されて計測時間を短縮できる。

## [ShortRunJob]属性指定

~~~C#
[ShortRunJob]//←これ
[MinColumn, MaxColumn]
public class StringConcatMesurement
{
//(略)
}
~~~

## [ShortRunJob]属性指定前の計測時間

~~~
Global total time: 00:10:46 (646.11 sec), executed benchmarks: 2
~~~

## [ShortRunJob]属性指定後の計測時間

~~~
Global total time: 00:00:57 (57.23 sec), executed benchmarks: 2
~~~

# メモリ使用量を計測する方法
下記[MemoryDiagnoser]属性指定することで、メモリ使用量を計測できる。

~~~C#
[MemoryDiagnoser]//←これ
[ShortRunJob]
[MinColumn, MaxColumn]
public class StringConcatMesurement
{
//(略)
}
~~~

下記表のAllocatedがメモリ使用量。

~~~
|            Method |           Mean |           Error |          StdDev |            Min |            Max |         Gen 0 |         Gen 1 |         Gen 2 |      Allocated |
|------------------ |---------------:|----------------:|----------------:|---------------:|---------------:|--------------:|--------------:|--------------:|---------------:|
| WithStringBuilder |       955.9 μs |        556.6 μs |        30.51 μs |       924.3 μs |       985.2 μs |      125.0000 |      124.0234 |      124.0234 |      784.04 KB |
|    WithStringType | 7,500,495.0 μs | 22,021,618.8 μs | 1,207,079.24 μs | 6,611,154.0 μs | 8,874,598.0 μs | 11078000.0000 | 10509000.0000 | 10509000.0000 | 39067409.16 KB |
~~~

# エクスポートする方法
出力形式を属性指定することでエクスポートが可能。形式の指定は下記のリンクを参考に。
[エクスポート機能(リンク)](https://benchmarkdotnet.org/articles/configs/exporters.html)も充実している。

HTMLでエクスポートした例は下記。

~~~C#
[HtmlExporter]//←これ
[MemoryDiagnoser]
[ShortRunJob]
[MinColumn, MaxColumn]
public class StringConcatMesurement
{
//(略)
}
~~~

コンソールに出力先が表示される。

~~~
// ***** BenchmarkRunner: Finish  *****

// * Export *
  BenchmarkDotNet.Artifacts\results\StudyBenchmarkDotNet.StringConcatMesurement-report.csv
  BenchmarkDotNet.Artifacts\results\StudyBenchmarkDotNet.StringConcatMesurement-report-github.md
  BenchmarkDotNet.Artifacts\results\StudyBenchmarkDotNet.StringConcatMesurement-report.html
~~~

HTMLファイルを開くと、こんな感じに出力される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/ed123632-b73a-8416-a4f1-dcf8e0d0c8a0.png)

# 参考にしたサイト
[BenchmarkDotNetの公式ドキュメント](https://benchmarkdotnet.org/articles/overview.html)
[How to benchmark C# code using BenchmarkDotNet](https://www.infoworld.com/article/3573782/how-to-benchmark-csharp-code-using-benchmarkdotnet.html)
[@neueccさんが書いたベンチマークの測り方](http://engineering.grani.jp/entry/2017/07/28/145035)
[文字列処理を高速に行う](https://dobon.net/vb/dotnet/string/stringbuilder.html)
[stringとStringBuilderの違い](https://www.atmarkit.co.jp/fdotnet/vblab/vbroom_02/vbroom_02_02.html)