web-dev-qa-db-ja.com

デリゲートとメソッドの呼び出しのパフォーマンス

この質問に続いて- C#を使用したパラメーターとしてのパスメソッド および個人的な経験の一部として、C#でのメソッドの呼び出しとデリゲートの呼び出しのパフォーマンスについてもう少し知りたいと思います。

デリゲートは非常に便利ですが、デリゲートを介して多くのコールバックを行うアプリがあり、これを書き直してコールバックインターフェイスを使用すると、速度が一桁向上しました。これは.NET 2.0でのことだったので、3と4で状況がどのように変わったのかわかりません。

デリゲートへの呼び出しはコンパイラ/ CLRで内部的にどのように処理され、これはメソッド呼び出しのパフォーマンスにどのように影響しますか?


[〜#〜] edit [〜#〜]-デリゲートとコールバックインターフェースの意味を明確にするため。

非同期呼び出しの場合、私のクラスは、呼び出し元がサブスクライブできるOnCompleteイベントと関連するデリゲートを提供できます。

または、呼び出し元が実装するOnCompleteメソッドを使用してICallbackインターフェイスを作成し、クラスに登録して、完了時にそのメソッドを呼び出すことができます(つまり、方法Javaこれらの処理)。

59
Paolo

私はその効果を見たことはありません-ボトルネックであることは確かに一度もありません。

これは非常に大まかですぐに使えるベンチマークで、(とにかく私のボックスでは)デリゲートが実際にインターフェイスよりも速いであることを示しています。

using System;
using System.Diagnostics;

interface IFoo
{
    int Foo(int x);
}

class Program : IFoo
{
    const int Iterations = 1000000000;

    public int Foo(int x)
    {
        return x * 3;
    }

    static void Main(string[] args)
    {
        int x = 3;
        IFoo ifoo = new Program();
        Func<int, int> del = ifoo.Foo;
        // Make sure everything's JITted:
        ifoo.Foo(3);
        del(3);

        Stopwatch sw = Stopwatch.StartNew();        
        for (int i = 0; i < Iterations; i++)
        {
            x = ifoo.Foo(x);
        }
        sw.Stop();
        Console.WriteLine("Interface: {0}", sw.ElapsedMilliseconds);

        x = 3;
        sw = Stopwatch.StartNew();        
        for (int i = 0; i < Iterations; i++)
        {
            x = del(x);
        }
        sw.Stop();
        Console.WriteLine("Delegate: {0}", sw.ElapsedMilliseconds);
    }
}

結果(.NET 3.5; .NET 4.0b2はほぼ同じです):

Interface: 5068
Delegate: 4404

今、私はデリゲートがインターフェイスよりも実際に速いことを意味するという特定の信念を持っていません...しかし、それは彼らが桁違いに遅くないことをかなり確信させます。また、デリゲート/インターフェイスメソッド内ではほとんど何もしていません。呼び出しごとの作業が増えるにつれて、呼び出しコストの違いが明らかになることは明らかです。

注意すべきことの1つは、単一のインターフェイスインスタンスのみを使用する場合に、新しいデリゲートを何度も作成しないことです。このcouldはガベージコレクションなどを引き起こすため、問題を引き起こします。インスタンスメソッドをループ内でデリゲートとして使用している場合、デリゲート変数をループ外で宣言する方が効率的です。単一のデリゲートインスタンスを作成して再利用します。例えば:

Func<int, int> del = myInstance.MyMethod;
for (int i = 0; i < 100000; i++)
{
    MethodTakingFunc(del);
}

より効率的です:

for (int i = 0; i < 100000; i++)
{
    MethodTakingFunc(myInstance.MyMethod);
}

これはあなたが見ていた問題だったのでしょうか?

74
Jon Skeet

CLR v 2以降、デリゲート呼び出しのコストは、インターフェイスメソッドに使用される仮想メソッド呼び出しのコストに非常に近いです。

Joel Pobar のブログを参照してください。

21
Pete Montgomery

デリゲートが仮想メソッドよりも大幅に高速または低速であることは完全に信じられません。どちらかといえば、デリゲートは無視できるほど高速でなければなりません。下位レベルでは、デリゲートは通常次のように実装されます(Cスタイルの表記を使用しますが、これは単なる例示であるため、マイナーな構文エラーはご容赦ください)。

struct Delegate {
    void* contextPointer;   // What class instance does this reference?
    void* functionPointer;  // What method does this reference?
}

デリゲートを呼び出すと、次のように機能します。

struct Delegate myDelegate = somethingThatReturnsDelegate();
// Call the delegate in de-sugared C-style notation.
ReturnType returnValue = 
    (*((FunctionType) *myDelegate.functionPointer))(myDelegate.contextPointer);

Cに翻訳されたクラスは次のようになります。

struct SomeClass {
    void** vtable;        // Array of pointers to functions.
    SomeType someMember;  // Member variables.
}

仮想関数を呼び出すには、次を実行します。

struct SomeClass *myClass = someFunctionThatReturnsMyClassPointer();
// Call the virtual function residing in the second slot of the vtable.
void* funcPtr = (myClass -> vtbl)[1];
ReturnType returnValue = (*((FunctionType) funcPtr))(myClass);

これらは基本的に同じですが、仮想関数を使用する場合は、間接の追加レイヤーを通過して関数ポインターを取得する点が異なります。ただし、最近のCPU分岐予測では関数ポインターのアドレスを推測し、関数のアドレスの検索と並行してターゲットを投機的に実行するため、この追加の間接レイヤーは多くの場合無料です。タイトなループでの仮想関数呼び出しは、インラインの直接呼び出しよりも遅くないことを発見しました(ただし、C#ではなくD) 。

19
dsimcha

いくつかのテストを行いました(.Net 3.5で...後で.Net 4を使用して自宅で確認します)。実際には、オブジェクトをインターフェイスとして取得してからメソッドを実行する方が、メソッドからデリゲートを取得してデリゲートを呼び出すよりも高速です。

変数が既に正しい型(インターフェイスまたはデリゲート)にあり、それを呼び出すだけでデリゲートが勝つことを考慮してください。

何らかの理由で、インターフェイスメソッド(おそらくは仮想メソッド)でデリゲートを取得すると、非常に遅くなります。

また、デリゲートを事前に格納できない場合(たとえば、Dispatchsなど)があることを考慮すると、インターフェイスが高速である理由が正当化される可能性があります。

結果は次のとおりです。

実際の結果を得るには、これをリリースモードでコンパイルし、Visual Studioの外部で実行します。

直接呼び出しを2回確認する
00:00:00.5834988
00:00:00.5997071

インターフェイス呼び出しを確認し、呼び出しごとにインターフェイスを取得する
00:00:05.8998212

インターフェース呼び出しを確認し、一度インターフェースを取得する
00:00:05.3163224

アクション(デリゲート)呼び出しを確認し、呼び出しごとにアクションを取得する
00:00:17.1807980

アクション(デリゲート)呼び出しを確認し、アクションを1回取得する
00:00:05.3163224

インターフェイスメソッドでアクション(デリゲート)を確認し、呼び出しごとに両方を取得する
00:03:50.7326056

インターフェイスメソッドを介したアクション(デリゲート)の確認、インターフェイスの取得、呼び出しごとのデリゲートの取得
00:03:48.9141438

インターフェイスメソッドでアクション(デリゲート)を確認し、両方を一度取得する
00:00:04.0036530

ご覧のとおり、直接呼び出しは非常に高速です。前にインターフェイスまたはデリゲートを保存し、それを呼び出すだけで本当に高速です。ただし、デリゲートを取得する必要があるのは、インターフェイスを取得するよりも遅くなります。インターフェイスメソッド(または仮想メソッド、確かではない)でデリゲートを取得するのは本当に遅いです(オブジェクトをインターフェイスとして取得する5秒と、アクションを取得するために同じことを行う4分とを比較してください)。

これらの結果を生成したコードは次のとおりです。

using System;

namespace ActionVersusInterface
{
    public interface IRunnable
    {
        void Run();
    }
    public sealed class Runnable:
        IRunnable
    {
        public void Run()
        {
        }
    }

    class Program
    {
        private const int COUNT = 1700000000;
        static void Main(string[] args)
        {
            var r = new Runnable();

            Console.WriteLine("To get real results, compile this in Release mode and");
            Console.WriteLine("run it outside Visual Studio.");

            Console.WriteLine();
            Console.WriteLine("Checking direct calls twice");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    r.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    r.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking interface calls, getting the interface at every call");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    IRunnable interf = r;
                    interf.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking interface calls, getting the interface once");
            {
                DateTime begin = DateTime.Now;
                IRunnable interf = r;
                for (int i = 0; i < COUNT; i++)
                {
                    interf.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) calls, getting the action at every call");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    Action a = r.Run;
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) calls, getting the Action once");
            {
                DateTime begin = DateTime.Now;
                Action a = r.Run;
                for (int i = 0; i < COUNT; i++)
                {
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }


            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) over an interface method, getting both at every call");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    IRunnable interf = r;
                    Action a = interf.Run;
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) over an interface method, getting the interface once, the delegate at every call");
            {
                DateTime begin = DateTime.Now;
                IRunnable interf = r;
                for (int i = 0; i < COUNT; i++)
                {
                    Action a = interf.Run;
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) over an interface method, getting both once");
            {
                DateTime begin = DateTime.Now;
                IRunnable interf = r;
                Action a = interf.Run;
                for (int i = 0; i < COUNT; i++)
                {
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }
            Console.ReadLine();
        }
    }

}
6
Paulo Zemek

デリゲートがコンテナであるという事実はどうですか?マルチキャスト機能はオーバーヘッドを追加しませんか?私たちが主題に取り組んでいる間に、このコンテナの側面をもう少し押し進めたらどうでしょうか? dがデリゲートである場合、d + = dを実行することを禁止しません。または(コンテキストポインター、メソッドポインター)ペアの任意に複雑な有向グラフの構築から。デリゲートが呼び出されたときにこのグラフがどのように走査されるかを説明するドキュメントはどこにありますか?

1
Dorian Yeager

ベンチテストは here にあります。

0
Latency