かなり頻繁にSO私は自分がどの実装が最も速いかを確認するために小さなコードの塊をベンチマークしていることに気付きます。
ベンチマークコードではジッターやガベージコレクターが考慮されていないというコメントを頻繁に目にします。
次の簡単なベンチマーク機能がありますが、ゆっくりと進化しています。
static void Profile(string description, int iterations, Action func) {
// warm up
func();
// clean up
GC.Collect();
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
使用法:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
// ... code being profiled
});
この実装には欠陥がありますか?実装Xが実装YよりもZ反復よりも速いことを示すのに十分ですか?これを改善する方法はありますか?
[〜#〜] edit [〜#〜](反復ではなく)時間ベースのアプローチが望ましいことは明らかです。時間チェックがパフォーマンスに影響を与えない実装はありますか?
変更された機能は次のとおりです。コミュニティで推奨されているように、コミュニティWikiを自由に修正してください。
static double Profile(string description, int iterations, Action func) {
//Run at highest priority to minimize fluctuations caused by other processes/threads
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Thread.CurrentThread.Priority = ThreadPriority.Highest;
// warm up
func();
var watch = new Stopwatch();
// clean up
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
return watch.Elapsed.TotalMilliseconds;
}
最適化を有効にしてリリースでコンパイルし、Visual Studioの外部でテストを実行するを確認してください。この最後の部分は重要です。JITは、リリースモードであっても、デバッガーが接続された状態で最適化が行われるためです。
GC.Collect
が戻る前に、ファイナライズは必ずしも完了しません。ファイナライズはキューに入れられ、別のスレッドで実行されます。このスレッドはテスト中もアクティブのままである可能性があり、結果に影響します。
テストを開始する前にファイナライズが完了したことを確認したい場合、 GC.WaitForPendingFinalizers
を呼び出して、ファイナライズキューがクリアされるまでブロックします。
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GCの相互作用を式から外したい場合は、「ウォームアップ」呼び出しを実行することをお勧めしますafter前ではなく、GC.Collect呼び出し。そうすれば、.NETには、OSから関数のワーキングセット用に十分なメモリが既に割り当てられていることがわかります。
繰り返しごとにインライン化されていないメソッド呼び出しを行っているので、テストしているものを空のボディと比較してください。また、メソッド呼び出しよりも数倍長い時間を確実に計ることができることを受け入れる必要があります。
また、プロファイリングしているものに応じて、一定の反復回数ではなく一定の時間だけタイミングベースの実行を行うこともできます。最適な実装のために非常に短い実行、および/または最悪の実装のために非常に長い実行が必要です。
デリゲートを渡すことはまったく避けたいです。
クロージャーの使用につながるサンプルコード:
public void Test()
{
int someNumber = 1;
Profiler.Profile("Closure access", 1000000,
() => someNumber + someNumber);
}
クロージャーについて知らない場合は、.NET Reflectorのこのメソッドを見てください。
このようなベンチマーク手法で克服するのが最も難しい問題は、Edgeのケースと予期しないものを考慮することだと思います。例-「2つのコードスニペットは、高いCPU負荷/ネットワーク使用率/ディスクスラッシングなどの下でどのように機能しますか?」特定のアルゴリズムが機能するかどうかを確認する基本的なロジックチェックに最適です大幅に他のアルゴリズムよりも高速です。ただし、ほとんどのコードパフォーマンスを適切にテストするには、その特定のコードの特定のボトルネックを測定するテストを作成する必要があります。
私はまだ、小さなコードブロックをテストしてもほとんど投資収益率がなく、単純な保守可能なコードの代わりに過度に複雑なコードを使用することを奨励できると言います。他の開発者、または6か月後の自分がすぐに理解できる明確なコードを作成すると、高度に最適化されたコードよりもパフォーマンス上のメリットが大きくなります。
1つだけでなく、ウォームアップのためにfunc()
を数回呼び出します。
実行環境がベンチマークに適しているかどうかを検出します(デバッガーが接続されているか、jit最適化が無効になっているために測定が正しくないかを検出するなど)。
コードの部分を個別に測定する(ボトルネックがどこにあるかを正確に確認するため)。
#1:について
デバッガーが接続されているかどうかを検出するには、プロパティSystem.Diagnostics.Debugger.IsAttached
(デバッガが最初にアタッチされていないが、しばらくしてからアタッチされる場合も処理することを忘れないでください).
Jit最適化が無効になっているかどうかを検出するには、プロパティDebuggableAttribute.IsJITOptimizerDisabled
関連するアセンブリ:
private bool IsJitOptimizerDisabled(Assembly assembly)
{
return Assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
.Select(customAttribute => (DebuggableAttribute) customAttribute)
.Any(attribute => attribute.IsJITOptimizerDisabled);
}
#2について:
これはさまざまな方法で実行できます。 1つの方法は、複数のデリゲートを提供し、それらのデリゲートを個別に測定することです。
#3について:
これも多くの方法で行うことができ、さまざまなユースケースでは非常に異なるソリューションが必要になります。ベンチマークを手動で起動する場合、コンソールへの書き込みで問題ない場合があります。ただし、ベンチマークがビルドシステムによって自動的に実行される場合、コンソールへの書き込みはおそらくそれほどうまくありません。
これを行う1つの方法は、ベンチマークの結果を、さまざまなコンテキストで簡単に使用できる厳密に型指定されたオブジェクトとして返すことです。
別のアプローチは、既存のコンポーネントを使用してベンチマークを実行することです。実際、私の会社では、ベンチマークツールをパブリックドメインにリリースすることにしました。核となるのは、ここにある他の回答のいくつかが示唆するように、ガベージコレクター、ジッター、ウォームアップなどを管理することです。また、上記で提案した3つの機能も備えています。 Eric Lippert blog で説明されているいくつかの問題を管理します。
これは、2つのコンポーネントが比較され、結果がコンソールに書き込まれる出力例です。この場合、比較される2つのコンポーネントは「KeyedCollection」および「MultiplyIndexedKeyedCollection」と呼ばれます。
NuGetパッケージ 、 サンプルNuGetパッケージ があり、ソースコードは GitHub で入手できます。 ブログ投稿 もあります。
お急ぎの場合は、サンプルパッケージを入手し、必要に応じてサンプルデリゲートを変更することをお勧めします。急いでいない場合は、ブログの投稿を読んで詳細を理解することをお勧めします。
また、実際の測定の前に「ウォームアップ」パスを実行して、JITコンパイラーがコードのジッターに費やす時間を除外する必要があります。
ベンチマーク対象のコードとそれが実行されるプラットフォームによっては、 コードのアライメントがパフォーマンスに与える影響 を考慮する必要がある場合があります。そのためには、テストを複数回(別々のアプリドメインまたはプロセスで)実行した外部ラッパーが必要になる場合があります。異なる方法で整列するようにベンチマークされています。完全なテスト結果は、さまざまなコード配置のベストケースとワーストケースのタイミングを提供します。
完全なベンチマークからガベージコレクションの影響を排除しようとしている場合、GCSettings.LatencyMode
?
そうではなく、func
で作成されたガベージの影響をベンチマークの一部にしたい場合は、テストの終了時(タイマー内)にコレクションを強制するべきではありませんか?
質問の基本的な問題は、単一の測定ですべての質問に答えることができるという仮定です。状況を効果的に把握するには、特にC#のようなガベージコレクション言語で複数回測定する必要があります。
別の答えは、基本的なパフォーマンスを測定する大丈夫な方法を提供します。
static void Profile(string description, int iterations, Action func) {
// warm up
func();
var watch = new Stopwatch();
// clean up
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}
ただし、この単一の測定ではガベージコレクションは考慮されません。さらに、適切なプロファイルは、多くの呼び出しにまたがるガベージコレクションの最悪の場合のパフォーマンスを考慮します(この番号は、VM func
の2つの異なる実装を比較します。)
static void ProfileGarbageMany(string description, int iterations, Action func) {
// warm up
func();
var watch = new Stopwatch();
// clean up
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}
また、1回だけ呼び出されるメソッドのガベージコレクションの最悪の場合のパフォーマンスを測定することもできます。
static void ProfileGarbage(string description, int iterations, Action func) {
// warm up
func();
var watch = new Stopwatch();
// clean up
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
watch.Start();
for (int i = 0; i < iterations; i++) {
func();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
watch.Stop();
Console.Write(description);
Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}
しかし、プロファイルする特定の可能な追加の測定値を推奨するよりも重要なのは、1種類の統計ではなく、複数の異なる統計を測定する必要があるという考えです。