web-dev-qa-db-ja.com

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

かなり頻繁に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 [〜#〜](反復ではなく)時間ベースのアプローチが望ましいことは明らかです。時間チェックがパフォーマンスに影響を与えない実装はありますか?

104
Sam Saffron

変更された機能は次のとおりです。コミュニティで推奨されているように、コミュニティ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は、リリースモードであっても、デバッガーが接続された状態で最適化が行われるためです。

92
Sam Saffron

GC.Collectが戻る前に、ファイナライズは必ずしも完了しません。ファイナライズはキューに入れられ、別のスレッドで実行されます。このスレッドはテスト中もアクティブのままである可​​能性があり、結果に影響します。

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

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
22
LukeH

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

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

また、プロファイリングしているものに応じて、一定の反復回数ではなく一定の時間だけタイミングベースの実行を行うこともできます。最適な実装のために非常に短い実行、および/または最悪の実装のために非常に長い実行が必要です。

15
Jonathan Rupp

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

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

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

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

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

6
Alex Yakunin

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

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

6
Paul Alexander

1つだけでなく、ウォームアップのためにfunc()を数回呼び出します。

5
Alexey Romanov

改善のための提案

  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 blog で説明されているいくつかの問題を管理します。

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

Etimo.Benchmarks - Sample Console Output

NuGetパッケージサンプルNuGetパッケージ があり、ソースコードは GitHub で入手できます。 ブログ投稿 もあります。

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

4
Joakim

また、実際の測定の前に「ウォームアップ」パスを実行して、JITコンパイラーがコードのジッターに費やす時間を除外する必要があります。

1
Alex Yakunin

ベンチマーク対象のコードとそれが実行されるプラットフォームによっては、 コードのアライメントがパフォーマンスに与える影響 を考慮する必要がある場合があります。そのためには、テストを複数回(別々のアプリドメインまたはプロセスで)実行した外部ラッパーが必要になる場合があります。異なる方法で整列するようにベンチマークされています。完全なテスト結果は、さまざまなコード配置のベストケースとワーストケースのタイミングを提供します。

1
Edward Brey

完全なベンチマークからガベージコレクションの影響を排除しようとしている場合、GCSettings.LatencyMode

そうではなく、funcで作成されたガベージの影響をベンチマークの一部にしたい場合は、テストの終了時(タイマー内)にコレクションを強制するべきではありませんか?

1
Danny Tuppeny

質問の基本的な問題は、単一の測定ですべての質問に答えることができるという仮定です。状況を効果的に把握するには、特に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種類の統計ではなく、複数の異なる統計を測定する必要があるという考えです。