web-dev-qa-db-fra.com

Performance des délégués appelants vs méthodes

Suite à cette question - Méthode Pass en tant que paramètre utilisant C # et une partie de mon expérience personnelle, j'aimerais en savoir un peu plus sur la performance de l'appel d'un délégué par rapport à l'appel d'une méthode en C #.

Bien que les délégués soient extrêmement pratiques, j'avais une application qui effectuait de nombreux rappels via des délégués. Lorsque nous avons réécrit cette procédure pour utiliser des interfaces de rappel, nous avons obtenu une amélioration de vitesse d'un ordre de grandeur. C'était avec .NET 2.0 donc je ne suis pas sûr de savoir comment les choses ont changé avec 3 et 4.

Comment les appels aux délégués sont-ils traités en interne dans le compilateur/CLR et comment cela affecte-t-il les performances des appels de méthode?


EDIT - Pour clarifier ce que je veux dire par délégués vs interfaces de rappel.

Pour les appels asynchrones, ma classe pourrait fournir un événement OnComplete et un délégué associé auquel l'appelant pourrait s'abonner. 

Alternativement, je pourrais créer une interface ICallback avec une méthode OnComplete que l'appelant implémentera, puis s'enregistrera auprès de la classe qui appellera ensuite cette méthode à son achèvement (c'est-à-dire la façon dont Java gère ces choses).

56
Paolo

Je n'ai pas vu cet effet - je n'ai certainement jamais vu que c'était un goulot d'étranglement.

Voici un point de repère très approximatif qui montre (sur ma boîte de toute façon) que les délégués sont en réalité plus rapides que des interfaces:

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

Résultats (.NET 3.5; .NET 4.0b2 est à peu près identique):

Interface: 5068
Delegate: 4404

Maintenant, je ne crois pas particulièrement que cela signifie que les délégués sont vraiment plus rapides que les interfaces ... mais je suis assez convaincu qu'ils ne sont pas d'un ordre de grandeur plus lent. De plus, cela ne fait presque rien avec la méthode delegate/interface. De toute évidence, le coût d’invocation fera de moins en moins de différence puisque vous travaillez de plus en plus par appel.

Une chose à faire est que vous ne créez pas plusieurs fois un nouveau délégué dans lequel vous n'utiliseriez qu'une seule instance d'interface. Cela pourrait causer un problème car cela provoquerait un garbage collection, etc. Si vous utilisez une méthode d'instance en tant que délégué dans une boucle, il sera plus efficace de déclarer la variable de délégué en dehors de la boucle, créez une seule instance de délégué et la réutiliser. Par exemple:

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

est plus efficace que:

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

Cela aurait-il pu être le problème que vous voyiez?

70
Jon Skeet

Depuis CLR v 2, le coût de l'invocation de délégué est très proche de celui de l'appel de méthode virtuelle, utilisé pour les méthodes d'interface.

Voir le blog de Joel Pobar .

19
Pete Montgomery

Je trouve complètement invraisemblable qu'un délégué soit sensiblement plus rapide ou plus lent qu'une méthode virtuelle. Au contraire, le délégué devrait être négligeable plus rapidement. À un niveau inférieur, les délégués sont généralement implémentés (par exemple, en utilisant la notation de style C, mais veuillez pardonner les erreurs de syntaxe mineures car il ne s'agit que d'une illustration):

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

Appeler un délégué fonctionne comme suit:

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

Une classe, traduite en C, serait quelque chose comme:

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

Pour appeler une fonction virtuelle, procédez comme suit:

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

Ils sont fondamentalement les mêmes, sauf que lorsque vous utilisez des fonctions virtuelles, vous passez par une couche supplémentaire d'indirection pour obtenir le pointeur de fonction. Cependant, cette couche d'indirection supplémentaire est souvent libre car les prédicteurs de branche d'UC modernes devineront l'adresse du pointeur de la fonction et exécuteront de manière spéculative sa cible en parallèle avec la recherche de l'adresse de la fonction. J'ai trouvé (bien qu'en D, pas C #) que les appels de fonctions virtuelles dans une boucle étroite ne soient pas plus lents que les appels directs non en ligne, à condition que, pour toute exécution de la boucle, ils résolvent toujours la même fonction .

18
dsimcha

J'ai fait quelques tests (dans .Net 3.5 ... plus tard, je vérifierai chez moi en utilisant .Net 4) . Le fait est que: Obtenir un objet en tant qu'interface puis exécuter la méthode est plus rapide que d'obtenir un déléguer à partir d'une méthode puis en appelant le délégué.

Considérant que la variable est déjà dans le bon type (interface ou délégué) et qu’elle est invoquée simplement, le délégué gagne.

Pour une raison quelconque, obtenir un délégué via une méthode d'interface (peut-être avec une méthode virtuelle) est BEAUCOUP plus lent.

Et, étant donné qu'il existe des cas où nous ne pouvons tout simplement pas pré-enregistrer le délégué (comme dans Dispatches, par exemple), cela peut justifier pourquoi les interfaces sont plus rapides.

Voici les résultats:

Pour obtenir des résultats concrets, compilez-le en mode Release et exécutez-le en dehors de Visual Studio.

Vérification des appels directs deux fois
00: 00: 00.5834988
00: 00: 00.5997071

Vérifier les appels d'interface, obtenir l'interface à chaque appel
00: 00: 05.8998212

Vérifier les appels d'interface, obtenir l'interface une fois
00: 00: 05.3163224

Vérification des appels d'action (délégué), obtention de l'action à chaque appel
00: 00: 17.1807980

Vérification des appels d'action (délégué), obtenir l'action une fois
00: 00: 05.3163224

Action de contrôle (délégué) sur une méthode d'interface, obtenant les deux à chaque appel
00: 03: 50.7326056

Action de vérification (délégué) sur une méthode d'interface, obtenant le interface une fois, le délégué à chaque appel
00: 03: 48.9141438

Action de contrôle (délégué) sur une méthode d'interface, obtenant les deux fois
00: 00: 04.0036530

Comme vous pouvez le constater, les appels directs sont très rapides… .. Il est très rapide de stocker l'interface ou le délégué auparavant, puis de ne l'appeler que très vite… Mais il est plus lent d'obtenir un délégué que d'avoir une interface . Avoir à obtenir un délégué sur une méthode d'interface (ou une méthode virtuelle, pas sûr) est vraiment lent (comparez les 5 secondes pour obtenir un objet en tant qu'interface aux presque 4 minutes de faire la même chose pour obtenir l'action).

Le code qui a généré ces résultats est ici:

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

Qu'en est-il du fait que les délégués sont des conteneurs? La capacité de multidiffusion n'ajoute-t-elle pas de temps système? Pendant que nous sommes sur le sujet, si nous poussions un peu plus loin cet aspect conteneur? Rien ne nous empêche, si d est un délégué, d’exécuter d + = d; ou de la construction d'un graphe dirigé arbitrairement complexe de paires (pointeur de contexte, pointeur de méthode). Où puis-je trouver la documentation décrivant la manière dont ce graphique est parcouru lorsque le délégué est appelé?

1
Dorian Yeager

Vous pouvez trouver des tests sur table ici .

0
Latency