はじめに
開発中にレビュー中の指摘でパフォーマンスが良い悪いの指摘があるが、実際どのくらい変わってくるのかとなどどうやって計測すれば良いのか、わからずモヤモヤしていた
たまたま以下記事を見て、こうやってパフォーマンス検証する
英語だけど、DeepLとかを使って駆使すれば内容を把握することはできる
参考記事
目的
どうすればパフォーマンスを数値として見ることができるのか知りたい
計測方法①
一番簡単に計測できる方法としては、StopWatchクラスを使うこと
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 1000; i++)
{
Math.Sqrt(i);
}
stopwatch.Stop();
Console.WriteLine($"Elapsed Time: {stopwatch.ElapsedMilliseconds} ms");
}
}
何が懸念なのか?
特別問題がある訳ではない
精密なパフォーマンスベンチマークを行うときには限界や注意点がある
主な注意点
・ウォームアップ不足
.NETは実行時にJIT(Just In Time)コンパイルされる
最初の実行はコンパイル時間も含むため、実際の実行パフォーマンスよりも遅くなる
単純にStopWatchクラスで一回だで計測すると、JITコンパイルのオーバヘッドも含まれてしまい、不正確な結果になる
・ガベージコレクション(GC)の影響
計測中にGCが発生すると、その間プログラムは一時停止するため、計測時間が不正確に長くなる可能性がある
・統計的な信頼性の欠如
Stopwatchクラスで数回計測しただけでは、OSのバックグラウンド処理、CPUのキャッシュ状態、スレッドスケジューリングなどさまざまな外部要因によるばらつきが排除できない
・計測対象が短すぎる場合
非常に短い処理(マイクロ秒、ナノ秒オーダー)をStopwatchで計測しようとすると、Stopwatch.Start()やStopwatch.Stop()メソッド呼び出し自体のオーバーヘッドが無視できなくなり、精度が低下
・手作業による手間とミス
正確な比較のためには、ウォームアップ、複数回の計測、統計処理などを手動で行う必要があり、手間がかかる上にミスも起こりやすい
・環境情報の欠如
パフォーマンスは実行環境(CPU、OS、.NETバージョンなど)に大きく依存する
Stopwatchだけでは、どのような環境で計測された結果なのかが記録されない
計測方法②
ベンチマークのライブラリを使う
Stopwatchクラスでの限界点に対する比較
・ウォームアップ不足
本計測の前にウォームアップ実行を自動で行い、JITコンパイルの影響を排除する
・ガベージコレクション(GC)の影響
GCの影響を考慮したり、計測の合間にGCを強制実行したりする機能を持っている
・統計的な信頼性の欠如
多数回の反復実行を行い、平均値、標準偏差、誤差などを計算し、統計的に信頼性の高い結果を提供
外れ値(異常に速い/遅い結果)を自動的に検出・除外する機能もある
・計測対象が短すぎる場合
このようなマイクロベンチマークをより正確に行うための仕組みを持っている
・手作業による手間とミス
正確な比較のためには、ウォームアップ、複数回の計測、統計処理などを手動で行う必要があるが、自動化してくれる
・環境情報の欠如
実行環境の情報を自動的にレポートに含めてくれる
使い方
手順①
BenchmarkDotNetのライブラリをプロジェクトにインストールする
コマンドで実行する場合
dotnet add package BenchmarkDotNet
手順②
以下のようにコードを作る
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<BenchmarkTest>();
}
}
public class BenchmarkTest
{
private List<int> numbers;
public BenchmarkTest()
{
// 1から10000までの整数を持つリストを作成
numbers = new List<int>();
for (int i = 1; i <= 10000; i++)
{
numbers.Add(i);
}
}
[Benchmark]
public int SumWithForLoop()
{
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
sum += numbers[i];
}
return sum;
}
[Benchmark]
public int SumWithLinq()
{
return numbers.Sum();
}
}
結果
色々と結果は表示されているがメインは以下
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
SumWithForLoop | 8,309.6271 ns | 81.2786 ns | 63.4570 ns | 8,286.1334 ns |
SumWithLinq | 5,602.1531 ns | 69.5001 ns | 61.6100 ns | 5,574.1417 ns |
見方
- Method
- 対象のメソッド名
- Mean
-
メソッドが1回処理にかかった平均実行時間
値が小さいほど速い
単位は通常、ns(ナノ秒)、us(マイクロ秒)、ms(ミリ秒)など、測定結果に応じて自動的に選ばれる
- Error
-
測定結果の信頼区間の半分の幅を示す
値が小さいほど、測定結果の信頼性が高い
(例)
SumWithLinqの誤差は69.6001ns
真の平均実行時間は、5,602.1531 ± 69.5001 ns の範囲にある可能性が高い
- StdDev
-
複数数回実行した際の測定結果のばらつき具合
値が小さいほど、各回の実行時間が安定してたことを意味する
値が大きい場合は、実行するたびに速度が大きく変動していたことを示唆する
- Median
- 複数回実行した測定結果を小さい順番に並べたとき、ちょうど真ん中にくる値
まとめ
結果から見ると、
ループを使うよりLinqを使った方がパフォーマンスは良い
ベンチマークの使い方については把握することができた
実務でどういうふうに使っていのかは検討していく必要がある
注意点については内部的の仕組みのな話になってきているため、あまりピントは来ていない
ただ数字の結果だけ見ると、確かにパフォーマンスが違っていることが見えているので、一定の効果はある
課題としては、もう少し限界点の内容については精査して、内容理解を深めていきたい
GCとか書いているけど、具体的に何が起こっているのか説明できるようになっていきたい