C#で小さなコードサンプルをベンチマークします。この実装を改善できますか?


104

私はかなり頻繁に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の実装がZの繰り返しよりも実装Yの方が速いことを示すのに十分ですか?これを改善する方法はありますか?

編集 (反復ではなく)時間ベースのアプローチが好ましいことはかなり明らかですが、時間チェックがパフォーマンスに影響しない実装は誰にもありますか?


BenchmarkDotNetも参照してください。
ベンハッチソン

回答:


95

変更された機能は次のとおりです。コミュニティによって推奨されているように、コミュニティ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はデバッガを接続して最適化を行うため、この最後の部分は重要です。


ループのオーバーヘッドを最小限に抑えるために、ループを10回など、何度かアンロールすることもできます。
Mike Dunlavey、2009年

2
Stopwatch.StartNewを使用するように更新しました。機能的な変更ではありませんが、コードを1行節約できます。
LukeH 2009年

1
@ルーク、大きな変化(+1できたらいいのに)@Mikeよくわかりません。virtualcallのオーバーヘッドは比較や割り当てよりもはるかに高くなるので、パフォーマンスの差はごくわずかになると思います
Sam Saffron

アクションに反復回数を渡し、そこでループを作成することをお勧めします(おそらく-展開されていても)。比較的短い操作を測定している場合、これが唯一のオプションです。そして、私は逆メトリックを見たいと思います-例えば、パス/秒のカウント。
Alex Yakunin

2
平均時間を表示することについてどう思いますか。このようなもの:Console.WriteLine( "平均経過時間{0}ミリ秒"、watch.ElapsedMilliseconds / iterations);
rudimenter

22

GC.Collect返却前にファイナライズが完了するとは限りません。ファイナライズはキューに入れられ、別のスレッドで実行されます。このスレッドはテスト中にまだアクティブであり、結果に影響を与える可能性があります。

テストを開始する前にファイナライズが完了していることを確認したい場合はGC.WaitForPendingFinalizers、を呼び出すと、ファイナライズキューがクリアされるまでブロックされます。

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

10
なぜGC.Collect()もう一度?
colinfang 2013年

7
@colinfang「ファイナライズ済み」のオブジェクトはファイナライザによってGCされないため。したがって、2つ目Collectは、「ファイナライズされた」オブジェクトも確実に収集されるようにするためのものです。
MAV、2014

15

GCの相互作用を方程式から外したい場合は、GC.Collect呼び出しの前ではなく、GC.Collect呼び出しの後に「ウォームアップ」呼び出しを実行することができます。これにより、.NETには、OSから関数のワーキングセット用に十分なメモリがすでに割り当てられていることがわかります。

反復ごとにインライン化されていないメソッド呼び出しを行っていることに注意してください。したがって、テストしているものを空の本文と比較してください。また、確実に時間を計測できるのは、メソッド呼び出しよりも数倍長いことだけを受け入れる必要があります。

また、プロファイリングする内容の種類によっては、特定の反復回数ではなく、特定の時間実行してタイミングベースにすることもできます。最適な実装には非常に短い実行時間を必要とし、最悪の場合には非常に長い実行時間を必要とします。


1
良い点は、時間ベースの実装を念頭に置いていますか?
サムサフラン

6

デリゲートを渡すことはまったく避けます:

  1. デリゲート呼び出しは〜仮想メソッド呼び出しです。安くない:.NETでの最小メモリ割り当ての約25%。詳細に興味がある場合は、このリンクなどを参照してください
  2. 匿名のデリゲートはクロージャーの使用につながる可能性があり、気付くことさえありません。繰り返しになりますが、クロージャーフィールドへのアクセスは、たとえばスタック上の変数へのアクセスよりも顕著です。

クロージャの使用につながるサンプルコード:

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

クロージャについて知らない場合は、.NET Reflectorでこのメソッドを確認してください。


興味深い点ですが、デリゲートを渡さない場合、再利用可能なProfile()メソッドをどのように作成しますか?メソッドに任意のコードを渡す他の方法はありますか?
アッシュ

1
「using(new Measurement(...)){...測定コード...}」を使用します。したがって、デリゲートを渡す代わりに、IDisposableを実装するMeasurementオブジェクトを取得します。code.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/…を
Alex Yakunin

これは、クロージャの問題にはつながりません。
Alex Yakunin

3
@AlexYakunin:リンクが壊れているようです。回答にMeasurementクラスのコードを含めてもらえますか?どのように実装しても、このIDisposableのアプローチでは、プロファイルを作成するコードを複数回実行できなくなると思います。ただし、複雑な(絡み合った)アプリケーションのさまざまな部分がどのように実行されているかを測定する必要がある状況では、測定が不正確で、異なる時間に実行すると一貫性が失われる可能性があることに注意してください。ほとんどのプロジェクトで同じアプローチを使用しています。
ShdNx

1
パフォーマンステストを数回実行する要件は非常に重要(ウォームアップ+複数の測定)なので、デリゲートを使用するアプローチにも切り替えました。さらに、クロージャーを使用しない場合、デリゲートの呼び出しは、の場合のインターフェースのメソッド呼び出しよりも高速IDisposableです。
Alex Yakunin 2012

6

このようなベンチマーク手法で克服するのが最も難しい問題は、エッジケースと予期しない問題を考慮することです。例-「2つのコードスニペットは、高いCPU負荷/ネットワーク使用率/ディスクスラッシングなどでどのように機能しますか?」これらは、特定のアルゴリズムが他のアルゴリズムよりも大幅に高速に動作するかどうかを確認するための基本的なロジックチェックに最適です。しかし、ほとんどのコードパフォーマンスを適切にテストするには、その特定のコードの特定のボトルネックを測定するテストを作成する必要があります。

コードの小さなブロックをテストすることは、多くの場合、投資収益率がほとんどなく、単純な保守可能なコードではなく、過度に複雑なコードの使用を奨励できると私はまだ言います。他の開発者、または私自身が6か月後の時点ですぐに理解できる明確なコードを書くと、高度に最適化されたコードよりもパフォーマンス上の利点が得られます。


1
重要なのは、実際に読み込まれる用語の1つです。実装が20%高速であることは重要な場合もあれば、100倍高速である必要がある場合もあります。透明度を参照してください。あなたに同意:stackoverflow.com/questions/1018407/...
サムサフラン

この場合、重要なのはロードされたすべてではありません。1つ以上の同時実行の実装を比較していて、これら2つの実装のパフォーマンスの差が統計的に有意でない場合は、より複雑な方法に取り組む価値はありません。
ポールアレクサンダー

5

func()ウォームアップは1回だけでなく、何度か呼びかけます。


1
意図は、jitコンパイルが確実に実行されるようにすることでしたが、測定の前にfuncを複数回呼び出すことでどのような利点がありますか?
サムサフラン

3
JITに最初の結果を改善する機会を与えるため。
アレクセイロマノフ

1
.NET JITは(Javaのように)時間の経過とともに結果を改善しません。最初の呼び出し時に、メソッドをILからアセンブリに1回だけ変換します。
マットウォーレン

4

改善のための提案

  1. 実行環境がベンチマークに適しているかどうかの検出(デバッガーが接続されているかどうか、またはjitの最適化が無効になっていて、測定結果が正しくないかどうかなど)。

  2. コードの一部を個別に測定する(ボトルネックがどこにあるかを正確に確認するため)。

  3. 異なるバージョン/コンポーネント/コードのチャンクを比較する(最初の文で、「...コードの小さなチャンクをベンチマークして、どの実装が最も高速かを確認する」と言います)。

#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つの方法は、異なるコンテキストで簡単に使用できる強く型付けされたオブジェクトとしてベンチマーク結果を返すことです。


Etimo.Benchmarks

別のアプローチは、既存のコンポーネントを使用してベンチマークを実行することです。実際、私の会社では、ベンチマークツールをパブリックドメインにリリースすることにしました。そのコアでは、ガベージコレクター、ジッター、ウォームアップなどを管理します。また、上記で提案した3つの機能も備えています。Eric Lippertブログで議論されているいくつかの問題を管理します。

これは、2つのコンポーネントが比較され、結果がコンソールに書き込まれる出力例です。この場合、比較される2つのコンポーネントは「KeyedCollection」および「MultiplyIndexedKeyedCollection」と呼ばれます。

Etimo.Benchmarks-コンソール出力のサンプル

ありNuGetパッケージサンプルNuGetパッケージとソースコードが利用可能であるGitHubのブログ投稿もあります。

お急ぎの場合は、サンプルパッケージを入手して、必要に応じてサンプルデリゲートを変更することをお勧めします。急いでいない場合は、ブログの投稿を読んで詳細を理解することをお勧めします。



1

ベンチマークするコードとそれが実行されるプラットフォームによっては、コードの配置がパフォーマンスに与える影響を考慮する必要がある場合があります。これを行うには、おそらくテストを複数回(個別のアプリドメインまたはプロセスで)実行する外部ラッパーが必要になります。場合によっては、最初に「パディングコード」を呼び出して強制的にJITコンパイルし、コードを別の方法で調整するためのベンチマーク。完全なテスト結果により、さまざまなコードアライメントのベストケースおよびワーストケースのタイミングが得られます。


1

ベンチマーク完了からガベージコレクションへの影響を排除しようとしている場合、設定する価値はありますGCSettings.LatencyModeか?

そうでない場合、で作成されfuncたガベージの影響をベンチマークの一部にしたい場合は、テストの最後に(タイマー内で)強制的に収集しないでください。


0

あなたの質問の基本的な問題は、単一の測定ですべての質問に答えることができるという仮定です。状況を効果的に把握するには、特に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);
}

ただし、この1回の測定ではガベージコレクションは考慮されません。適切なプロファイルは、多くの呼び出しにまたがるガーベッジコレクションの最悪の場合のパフォーマンスをさらに説明します(VMは、残されたガーベッジを収集せずに終了できるため、この数は無意味ですが、の2つの異なる実装を比較するのに役立ちfuncます。)

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種類の統計だけでなく、複数の異なる統計を測定するという考えです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.