17

たまたま、コード設計に関する1つの質問があります。たとえば、「変更」する可能性のあるいくつかの関数を呼び出す「テンプレート」メソッドが1つあります。直感的なデザインは、「テンプレートデザインパターン」に従うことです。変更関数を、サブクラスでオーバーライドされる「仮想」関数として定義します。または、「仮想」なしでデリゲート関数を使用することもできます。デリゲート関数は、カスタマイズできるように挿入されています。

もともと、2番目の「デリゲート」方法は「仮想」方法よりも高速だと思っていましたが、一部のコーディングスニペットはそれが正しくないことを証明しています。

以下のコードでは、最初のDoSomethingメソッドは「テンプレートパターン」に従います。仮想メソッドIsTokenCharを呼び出します。2番目のDoSomthingメソッドは、仮想関数に依存しません。代わりに、パスインデリゲートがあります。私のコンピューターでは、最初のDoSomthingは常に2番目のDoSomthingよりも高速です。結果は1645:1780のようになります。

「仮想呼び出し」は動的バインディングであり、直接委任呼び出しよりも時間のかかる作業ですよね?しかし、結果はそうではないことを示しています。

誰でもこれを説明できますか?

using System;
using System.Diagnostics;

class Foo
{
    public virtual bool IsTokenChar(string word)
    {
        return String.IsNullOrEmpty(word);
    }

    // this is a template method
    public int DoSomething(string word)
    {
        int trueCount = 0;
        for (int i = 0; i < repeat; ++i)
        {
            if (IsTokenChar(word))
            {
                ++trueCount;
            }
        }
        return trueCount;
    }

    public int DoSomething(Predicate<string> predicator, string word)
    {
        int trueCount = 0;
        for (int i = 0; i < repeat; ++i)
        {
            if (predicator(word))
            {
                ++trueCount;
            }
        }
        return trueCount;
    }

    private int repeat = 200000000;
}

class Program
{
    static void Main(string[] args)
    {
        Foo f = new Foo();

        {
            Stopwatch sw = Stopwatch.StartNew();
            f.DoSomething(null);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }

        {
            Stopwatch sw = Stopwatch.StartNew();
            f.DoSomething(str => String.IsNullOrEmpty(str), null);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }
    }
}
4

7 に答える 7

21

それぞれの場合に何が必要かを考えてください。

仮想通話

  • 無効性を確認する
  • オブジェクト ポインターから型ポインターに移動する
  • 命令テーブルでメソッドアドレスを検索
  • メソッドがオーバーライドされていない場合、基本型に移動しますか? 正しいメソッドアドレスが見つかるまで再帰します。(そうは思いません - 下部の編集を参照してください。)
  • 元のオブジェクト ポインターをスタックにプッシュする ("this")
  • 呼び出し方法

代理呼び出し

  • 無効性を確認する
  • オブジェクト ポインターから呼び出しの配列に移動します (すべてのデリゲートは潜在的にマルチキャストです)。
  • 配列をループし、呼び出しごとに:
    • フェッチ メソッド アドレス
    • ターゲットを第一引数として渡すかどうかを調べる
    • 引数をスタックにプッシュします (すでに行われている可能性があります - 不明)
    • 必要に応じて (呼び出しが開いているか閉じているかに応じて) 呼び出しターゲットをスタックにプッシュします。
    • 呼び出し方法

単一呼び出しの場合にループが発生しないように最適化が行われている可能性がありますが、それでも非常に簡単なチェックが必要です。

しかし、基本的には、デリゲートに関連する間接性が同じくらいあります。仮想メソッド呼び出しで私が確信していないビットを考えると、非常に深い型階層でオーバーライドされていない仮想メソッドへの呼び出しが遅くなる可能性があります...試してみて、答えを編集します.

編集:継承階層の深さ(最大20レベル)、「最も派生したオーバーライド」のポイント、および宣言された変数の型の両方で遊んでみましたが、どれも違いを生むようには見えません。

編集:(渡された)インターフェイスを使用して元のプログラムを試してみました-これは、デリゲートとほぼ同じパフォーマンスになります。

于 2008-10-19T06:49:29.913 に答える
12

ジョンスキートの応答にいくつかの修正を追加したかっただけです:

仮想メソッド呼び出しでは、null チェックを行う必要はありません (ハードウェア トラップで自動的に処理されます)。

また、オーバーライドされていないメソッドを見つけるために継承チェーンをたどる必要もありません (それが仮想メソッド テーブルの目的です)。

仮想メソッド呼び出しは、基本的に、呼び出し時の間接化の 1 つの余分なレベルです。テーブル ルックアップとそれに続く関数ポインタ呼び出しのため、通常の呼び出しよりも遅くなります。

デリゲート呼び出しには、余分なレベルの間接化も含まれます。

デリゲートの呼び出しでは、DynamicInvoke メソッドを使用して動的呼び出しを実行していない限り、引数を配列に入れる必要はありません。

デリゲート呼び出しには、問題のデリゲート型でコンパイラが生成した Invoke メソッドを呼び出す呼び出しメソッドが含まれます。predicator(value) への呼び出しは、predicator.Invoke(value) に変換されます。

次に、Invoke メソッドが JIT によって実装され、関数ポインター (デリゲート オブジェクトに内部的に格納されます) を呼び出します。

あなたの例では、渡したデリゲートは、実装がインスタンス変数やローカルにアクセスしないため、コンパイラによって生成された静的メソッドとして実装されている必要があるため、ヒープから「this」ポインターにアクセスする必要はありません。

デリゲート関数呼び出しと仮想関数呼び出しのパフォーマンスの違いはほとんど同じである必要があり、パフォーマンス テストでは、それらが非常に近いことが示されています。

違いは、マルチキャストによる追加のチェックと分岐の必要性によるものである可能性があります (John が提案したように)。もう 1 つの理由として、JIT コンパイラが Delegate.Invoke メソッドをインライン化せず、Delegate.Invoke の実装が仮想メソッド呼び出しの実行時に実装と同様に引数を処理しないことが考えられます。

于 2009-02-06T00:43:51.080 に答える
8

仮想呼び出しは、メモリ内の既知のオフセットで 2 つのポインターを逆参照しています。実際には動的バインディングではありません。実行時にメタデータを反映して適切なメソッドを検出するコードはありません。コンパイラは、this ポインターに基づいて、呼び出しを行うための命令をいくつか生成します。実際、仮想呼び出しは単一の IL 命令です。

述語呼び出しは、述語をカプセル化する匿名クラスを作成しています。そのクラスはインスタンス化する必要があり、述語関数ポインターが null かどうかを実際にチェックするために生成されるコードがいくつかあります。

両方の IL コンストラクトを確認することをお勧めします。2 つの DoSomthing のそれぞれを 1 回呼び出して、上記のソースの簡略化されたバージョンをコンパイルします。次に、ILDASM を使用して、各パターンの実際のコードを確認します。

(そして、正しい用語を使用していないために、私は反対票を投じられると確信しています:-))

于 2008-10-19T05:57:50.220 に答える
3

1000 語に相当するテスト結果: http://kennethxu.blogspot.com/2009/05/strong-typed-high-performance_15.html

于 2009-06-26T01:29:22.047 に答える
1

仮想メソッドをオーバーライドするメソッドがないため、JITがこれを認識し、代わりに直接呼び出しを使用できる可能性があります。

このような場合は、パフォーマンスを推測するよりも、実行したとおりにテストする方が一般的に優れています。デリゲートの呼び出しがどのように機能するかについて詳しく知りたい場合は、ジェフリー・リッチターによる優れた本「CLR ViaC#」をお勧めします。

于 2008-10-19T04:40:34.243 に答える
1

それがあなたの違いのすべてを説明しているとは思えませんが、違いのいくつかを説明しているかもしれない私の頭の上の1つのことは、仮想メソッドディスパッチがすでにthisポインターを準備しているということです。デリゲートを介して呼び出す場合、thisポインタはデリゲートからフェッチする必要があります。

このブログ記事によると、.NETv1.xでは違いがさらに大きかったことに注意してください。

于 2008-10-19T05:27:40.950 に答える
0

仮想オーバーライドには、コンパイル時にハードコーディングされて完全に最適化された、ある種のリダイレクトテーブルなどがあります。それは非常に速く、石に設定されています。

デリゲートは動的であり、常にオーバーヘッドがあり、それらもオブジェクトのように見えるため、合計されます。

これらの小さなパフォーマンスの違いについて心配する必要はありません(軍隊向けのパフォーマンスが重要なソフトウェアを開発しない限り)。ほとんどの場合、優れたコード構造が最適化に勝ります。

于 2008-10-19T05:26:40.423 に答える