it-swarm.com.de

Leistung von aufrufenden Delegaten im Vergleich zu Methoden

Nach dieser Frage - Pass Method als Parameter mit C # und einigen meiner persönlichen Erfahrungen möchte ich etwas mehr über die Leistung eines Aufrufs eines Delegaten erfahren, als nur eine Methode in C # aufzurufen.

Obwohl Delegierte äußerst bequem sind, hatte ich eine App, die viele Rückrufe über Delegierte durchführte. Als wir dies umschrieben hatten, um Callback-Schnittstellen zu verwenden, wurde die Geschwindigkeit um ein Vielfaches gesteigert. Das war mit .NET 2.0, daher bin ich mir nicht sicher, wie sich die Dinge mit 3 und 4 geändert haben.

Wie werden Aufrufe an Delegaten intern im Compiler/CLR behandelt und wie wirkt sich dies auf die Leistung von Methodenaufrufen aus?


EDIT - Um zu klären, was ich unter Delegaten und Callback-Schnittstellen verstehe.

Bei asynchronen Aufrufen könnte meine Klasse ein OnComplete-Ereignis und einen zugeordneten Delegaten bereitstellen, den der Aufrufer abonnieren kann. 

Alternativ könnte ich eine ICallback-Schnittstelle mit einer OnComplete-Methode erstellen, die der Aufrufer implementiert und sich dann bei der Klasse registriert, die diese Methode dann nach Beendigung aufruft (d. H. Wie Java diese Dinge behandelt).

56
Paolo

Ich habe diesen Effekt nicht gesehen - ich habe sicher noch nie einen Engpass gesehen.

Hier ist ein sehr rauer Benchmark, der zeigt, dass Delegierte tatsächlich schneller als Schnittstellen sind:

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);
    }
}

Ergebnisse (.NET 3.5; .NET 4.0b2 ist ungefähr gleich):

Interface: 5068
Delegate: 4404

Nun habe ich kein besonderes Vertrauen, dass Delegierte wirklich schneller als Schnittstellen sind ..., aber ich bin ziemlich überzeugt, dass sie nicht um eine Größenordnung langsamer sind. Darüber hinaus macht dies fast nichts innerhalb der Delegate/Interface-Methode. Offensichtlich werden die Anrufkosten immer weniger ausfallen, da Sie mehr und mehr Arbeit pro Anruf erledigen.

Seien Sie vorsichtig, wenn Sie nicht mehrmals einen neuen Delegierten erstellen, bei dem Sie nur eine einzelne Schnittstelleninstanz verwenden. Dieses könnte ein Problem verursachen, da dies zu einer Garbage Collection führen würde. Wenn Sie eine Instanzmethode als Delegat innerhalb einer Schleife verwenden, wird es effizienter sein, die Delegatvariable außerhalb der Schleife zu deklarieren eine einzelne Delegat-Instanz und verwenden Sie sie erneut. Zum Beispiel:

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

ist effizienter als:

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

Könnte dies das Problem gewesen sein, das Sie gesehen haben?

70
Jon Skeet

Seit CLR v 2 liegen die Kosten des Delegatenaufrufs sehr nahe an denjenigen des virtuellen Methodenaufrufs, der für Schnittstellenmethoden verwendet wird.

Siehe Joel Pobar s Blog.

19
Pete Montgomery

Ich finde es absolut unplausibel, dass ein Delegierter wesentlich schneller oder langsamer ist als eine virtuelle Methode. Wenn überhaupt, sollte der Delegierte vernachlässigbar schneller sein. Auf einer niedrigeren Ebene werden Delegaten normalerweise so implementiert (wie in der C-Notation, aber bitte vergeben Sie kleinere Syntaxfehler, da dies nur eine Illustration ist):

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

Das Aufrufen eines Delegierten funktioniert wie folgt:

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

Eine Klasse, übersetzt in C, würde ungefähr so ​​aussehen:

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

Um eine vritual-Funktion aufzurufen, würden Sie Folgendes tun:

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);

Sie sind im Grunde die gleichen, mit der Ausnahme, dass Sie bei Verwendung virtueller Funktionen eine zusätzliche Ebene der Indirektion durchlaufen, um den Funktionszeiger zu erhalten. Diese zusätzliche Umleitungsschicht ist jedoch häufig frei, da moderne CPU-Verzweigungsprädiktoren die Adresse des Funktionszeigers erraten und ihr Ziel spekulativ ausführen, während sie die Adresse der Funktion nachschlagen. Ich habe festgestellt (wenn auch nicht in D, nicht in C #), dass virtuelle Funktionsaufrufe in einer engen Schleife nicht langsamer sind als nicht inline-Direktaufrufe, vorausgesetzt, dass sie für einen bestimmten Lauf der Schleife immer in dieselbe echte Funktion aufgelöst werden .

18
dsimcha

Ich habe einige Tests gemacht (in .Net 3.5 ... später überprüfe ich zu Hause mit .Net 4) . Die Tatsache ist: Ein Objekt als Schnittstelle zu erhalten und dann die Methode auszuführen, ist schneller als ein Delegieren Sie von einer Methode aus, und rufen Sie dann den Delegierten auf.

Wenn man bedenkt, dass die Variable bereits im richtigen Typ ist (Schnittstelle oder Delegat), wird der Delegat durch einfaches Aufrufen gewonnen.

Aus irgendeinem Grund ist es VIEL langsamer, einen Delegaten über eine Schnittstellenmethode (möglicherweise über eine beliebige virtuelle Methode) zu erhalten.

Und wenn man bedenkt, dass es Fälle gibt, in denen wir den Delegaten (wie beispielsweise in Dispatches) nicht vorab speichern können, kann dies rechtfertigen, warum Schnittstellen schneller sind.

Hier sind die Ergebnisse:

Um echte Ergebnisse zu erhalten, kompilieren Sie dies im Freigabemodus und führen Sie es außerhalb von Visual Studio aus.

Direkte Anrufe zweimal überprüfen
00: 00: 00.5834988
00: 00: 00.5997071

Überprüfen von Schnittstellenaufrufen, Abrufen der Schnittstelle bei jedem Anruf
00: 00: 05.8998212

Schnittstellenaufrufe prüfen, Schnittstelle einmalig abrufen
00: 00: 05.3163224

Überprüfen von Aktion (Delegieren) von Aufrufen, Abrufen der Aktion bei jedem Anruf
00: 00: 17.1807980

Action (delegieren) -Aufrufe prüfen, Action einmal abrufen
00: 00: 05.3163224

Action (Delegate) über eine Schnittstellenmethode prüfen, beides unter .__ abrufen. bei jedem Anruf
00: 03: 50.7326056

Action (Delegate) über eine Schnittstellenmethode prüfen, .__ abrufen. Schnittstelle einmalig, der Delegierte bei jedem Anruf
00: 03: 48,9141438

Action (Delegate) über eine Schnittstellenmethode prüfen, beides einmal abrufen
00: 00: 04.0036530

Wie Sie sehen, sind die direkten Anrufe wirklich schnell. Das Speichern der Schnittstelle oder des Delegaten vor dem Speichern und das Anrufen ist nur sehr schnell. Aber es ist langsamer, einen Delegierten zu erhalten, als eine Schnittstelle zu erhalten Einen Delegaten über eine Schnittstellenmethode (oder eine virtuelle Methode, nicht sicher) zu bekommen, ist wirklich langsam (vergleichen Sie die 5 Sekunden, um ein Objekt als Schnittstelle zu erhalten, mit den fast 4 Minuten, die das Gleiche tun, um die Aktion auszuführen).

Der Code, der diese Ergebnisse generiert hat, ist hier:

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();
        }
    }

}
5
Paulo Zemek

Was ist mit der Tatsache, dass Delegierte Container sind? Führt die Multicast-Fähigkeit nicht zu zusätzlichen Kosten? Wenn wir zu diesem Thema sind, was ist, wenn wir diesen Containeraspekt ein wenig weiter schieben? Nichts verbietet uns, wenn d ein Delegierter ist, d + = d auszuführen. oder durch Erstellen eines beliebig komplexen gerichteten Graphen von (Kontextzeiger, Methodenzeiger) Paaren. Wo finde ich die Dokumentation, in der beschrieben wird, wie dieses Diagramm durchlaufen wird, wenn der Delegat aufgerufen wird?

1
Dorian Yeager

Bench-Tests finden Sie hier .

0
Latency