概要
BenchmarkDotNetの紹介と使い方を掲載する。
BenchmarkDotNetはメソッドのパフォーマンスを簡単に計測できるライブラリです。
統計的な情報をレポートしてくれるため、関数のパフォーマンスを調査する業務で役立ちそう。
目次
BenchmarkDotNetとは?
BenchmarkDotNetは、メソッドをベンチマークに変換し、そのパフォーマンスを追跡し、
再現性のある測定実験を共有するのに役立ちます。
上記はGitHubの記載内容の機械翻訳
サポート対象は下記。.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
エクスポート機能も充実している。
設定手順
下記ドキュメントのOverViewに従って、パフォーマンス計測を行う。
BenchmarkDotNetの公式ドキュメント
1.NuGetパッケージをインストール
Visual Studioを立ち上げ、「コンソールアプリケーション」を選択する。
下記のNuGetパッケージを追加する。
NuGetリンク
2.計測対象のメソッドを含むクラスを作成する
扱う題材(計測対象)
stringとStringBuilderの文字列結合の処理時間を比較する。
期待結果は「StringBuilderの方がパフォーマンスが優れる」こと。
※下記サイトで既に検証済み
文字列処理を高速に行う
StringBuilderがパフォーマンスが優れる理由
下記の違いにより、扱う文字の数が増えるとStringBuilderの方がパフォーマンスに優れることが多い。 ・string型は編集不可なため、文字列操作の度に新たなインスタンスを作る。 ・StringBuilderは編集可能であるため、文字列操作で新たなインスタンスを作らない。 stringとStringBuilderの違い計測対象のクラスを定義する
計測対象のメソッド含むクラスを定義する。
対象メソッドには[Benchmark]属性を指定する。
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;
}
}
メイン関数からコールする
using BenchmarkDotNet.Running;
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<StringConcatMesurement>();
}
}
これで準備完了!!実行する前に一点だけ注意事項。
[リリースビルド]で実行すること。
[デバックビルド]の場合は、下記のように実行時エラーになる。
ちなみに、このままの状態で実行すると私の環境で10分以上掛かった。
ベンチマーク計測に正確さが必要ない場合、[ShortRunJob]属性をつけて下さい(後述の項 計測時間の短縮をする方法を参照)。
計測開始時
計測終了時
計測結果を確認する
下記の結果がコンソールに表示される。
・実行環境
・統計情報の表(関数名、平均値、エラー、標準偏差)
・統計情報のラベルの説明
// * 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に比べると計測結果のバラツキが小さいことが分かる。
余談
コンソール出力内容はログに記録されている。
ソリューションの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) | @@@
---------------------------------------------------
サマリーのカスタマイズ実装
[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]属性指定
[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]属性指定することで、メモリ使用量を計測できる。
[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 |
エクスポートする方法
出力形式を属性指定することでエクスポートが可能。形式の指定は下記のリンクを参考に。
エクスポート機能(リンク)も充実している。
HTMLでエクスポートした例は下記。
[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ファイルを開くと、こんな感じに出力される。
参考にしたサイト
BenchmarkDotNetの公式ドキュメント
How to benchmark C# code using BenchmarkDotNet
@neueccさんが書いたベンチマークの測り方
文字列処理を高速に行う
stringとStringBuilderの違い