neuecc先生の、C#でTypeをキーにしたDictionaryのパフォーマンス比較と最速コードの実装
に記載のあった、BenchmarkDotNetが個人的に相当刺さった。なので、備忘録がてらまとめてみたいと思うのでお付き合い頂ければ幸い。
#これてばなによ?
例えば、Aと言う実装と、Bと言う実装が有ってどっちが早いの?とか
HogeMogeの時間的コストはどんなモノなのかみたいなとき、Stopwatch
とか使ってベンチマーク取るわけだけど、実際問題
- Runner(ドライバ)部分はほぼいつも一緒
- オーバーヘッドの除去が面倒
- サンプリングして統計か処理取るのも面倒
などなど、まぁ面倒ばっかり多くてしんどかった。
しかも環境が偏って、全く別のところで差異が出て有りもしない理由を探ったり、逆に本来有意に差が付くはずなのに付かなくて見落としたりと、まぁ面倒だし罠も多い。
で、その辺のコトをまとめてしてくれる便利フレームワークが、BenchmarkDotNetてかんじです。
#使うための下準備は?
安心と信頼のNugetから落としてくればOK。
コマンドラインにPM> Install-Package BenchmarkDotNet
とぶち込むか、NugetのギャラリーでBenchmarkDotNet
を探せば良い感じ。
蛇足ながら使える環境は、.NET Framerowk 4.6
以降か、.NET Core 1.1
以降となってる。
また、英語版の公式ドキュメントはこのへんにある。
#どーやって使うの?
試しに、List<int>
とint[]
の読み取りアクセスコストの差異があるのか?有るとしたらどれほどなのかみたいなシナリオをこさえて試してみよかと。
##コードの書き方
ベンチマークを取りたいコードはこのように書く。
public class SomeTest
{
public int[] _array;
public List<int> _list;
public SomeTest()
{
_array = Enumerable.Range(0, 10000).ToArray();
_list = _array.ToList();
}
[Benchmark]
public int UseArrayFor()
{
var accum = 0;
for (int i = 0; i < _array.Length; i++)
{
accum += _array[i];
}
return accum;
}
[Benchmark]
public int UseArrayForEach()
{
var accum = 0;
foreach (var i in _array)
{
accum += i;
}
return accum;
}
[Benchmark]
public int UseListFor()
{
var accum = 0;
for (int i = 0; i < _list.Count; i++)
{
accum += _list[i];
}
return accum;
}
[Benchmark]
public int UseListForEach()
{
var accum = 0;
foreach (var i in _list)
{
accum += _list[i];
}
return accum;
}
}
上記に提示したコードのように、ベンチマークの対象となるメソッドには、[Benchmark]
属性を付与しておく。
また、以下のような制約がある。
- 引数は取れない
- publicメソッド必須(まぁ当たり前)
逆に、戻り値は会っても無くても良い。
#ベンチマークの実行
実行する方法は、エントリポイントに
static void Main(string[] args)
{
BenchmarkRunner.Run<SomeTest>();
}
これで実行するだけ。
ただ、コンフィグ書いてないときは、実行時の環境(Debug/Relase/X86/x64)に依拠するのでその辺は注意。
個人的には、Releaseビルドデバッガ無し実行あたりで良いと思います。
(ただ、Roslynでランタイムコンパイルしてベンチマーク本体は多分別プロセスで動いてるような雰囲気はある)
##ちょっとした補足
BenchmarkDotnetはできる限り正確に結果を出そうとする。
で、正確に結果を出そうとすれば手数を増やして結果を安定させる必要があるけど、当然時間がかかる。
なので、
- 実行可能か?
- 結果が予測通りか?(予測できないのなら明らかにおかしな結果が出ないか)
みたいな検証は事前に行う必要があるわけでそこまで時間をかけたくない。
ソのようなときは、ベンチマークメソッドのあるSomeTest
クラスに、
[DryJob]
属性や、[ShortRunJob]
を付けることで、実行時間を短縮出来る(当然、正確性とのトレードオフはある)
[DryJob]
は、文字通り試運転なので、結果を計測するには全く向かない。だけど、テストベンチ自体がまともに動くか確認するには有用だと思う。[ShortRunJob]
は文字通り短縮した形でテストを実行するので、パラメタライズなベンチマークで当たりを付けたり、事前実験するならこれが良いかと思います。
#どーいう結果が出てくるのか?
実行すると、コンソールが出てきて先の例だと、以下のような結果が出てくる
ココより前は、実行中のログが出てくる。(結構膨大)
// * Export *
BenchmarkDotNet.Artifacts\results\SomeTest-report.csv
BenchmarkDotNet.Artifacts\results\SomeTest-report-github.md
BenchmarkDotNet.Artifacts\results\SomeTest-report.html
この部分は、結果のエクスポート先がどこにあるか
// * Detailed results *
SomeTest.UseArrayFor: DefaultJob
Runtime = .NET Framework 4.7 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2101.1; GC = Concurrent Workstation
Mean = 5.4402 us, StdErr = 0.0015 us (0.03%); N = 12, StdDev = 0.0052 us
Min = 5.4311 us, Q1 = 5.4366 us, Median = 5.4399 us, Q3 = 5.4422 us, Max = 5.4509 us
IQR = 0.0055 us, LowerFence = 5.4284 us, UpperFence = 5.4504 us
ConfidenceInterval = [5.4335 us; 5.4469 us] (CI 99.9%), Margin = 0.0067 us (0.12% of Mean)
Skewness = 0.41, Kurtosis = 2.58
で次は、ベンチマーク毎の結果の詳細が出てくる。ここからわかるのは、
- Runtime:ココではドトネトフレームワーク4.7
- JIT:32bit のレガシー
- GC:コンカレントワークステーション)
みたいに環境系のレポートに続いて実行結果の統計情報が出てくる
1. Mean:算術平均値
2. StdErr:標準誤差
3. N:試行回数
4. StdDev:標準偏差
5. Min、Q1,Median,Q3,Max:四分位数
6. IQR:四分位範囲
7. LowerFence,UpperFence:外れ値としてフィルタする下限と上限 1
8. ConfidenceInterval:信頼区画(ココでは、99.9%)
9. Margin:信頼区画とMeanの片側マージン
10. Skewness:歪度
11. Kurtosis:尖度
この中で一番意味を持つのが、まず間違いなくMean
。
これの比較で複数のベンチマークをしたときとか、相対的な差をみることが出来る。
ただ、ココでは深く扱わないけど、取ったサンプルが暴れているかどうか 2はある程度気にした方が良い気がする。
で、続いて
Method | Mean | Error | StdDev |
---------------- |----------:|----------:|----------:|
UseArrayFor | 5.440 us | 0.0067 us | 0.0052 us |
UseArrayForEach | 4.156 us | 0.0108 us | 0.0101 us |
UseListFor | 19.054 us | 0.0467 us | 0.0437 us |
UseListForEach | 38.653 us | 0.1514 us | 0.1416 us |
こんな感じで、サマリが出てくる。
#まとめ
今回は、基本的な使い方をまとめてみました。
時間があれば、Rを使ったPlottingや、パラメタライズテストの実行方法、ランタイムにJitを別個指定したテストの実行方法など
応用的な解説が出来たらと思います。