73 votes

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

Suite à cette question - http://stackoverflow.com/questions/2082615/pass-method-as-parameter-using-c et une partie de mon expérience personnelle, je voudrais en savoir un peu plus sur les résultats de l'appel d'un délégué vs juste appeler une méthode en C#.

Bien que les délégués sont très pratique, j'avais une application qui fait de nombreux rappels via les délégués et quand on réécrit cette utilisation de rappel des interfaces, nous avons un ordre de grandeur de la vitesse d'amélioration. Ce fut avec .NET 2.0 donc je ne suis pas sûr de savoir comment les choses ont changé avec 3 et 4.

Comment sont les appels aux délégués géré en interne par le compilateur/CLR et comment cela affecte les performances de la méthode des appels?


MODIFIER - Pour clarifier ce que je veux dire par les délégués vs rappel des interfaces.

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

Sinon j'ai pu créer un ICallback interface avec un OnComplete méthode que l'appelant met en œuvre, puis enregistre avec la classe qui va ensuite appeler cette méthode à la fin (c'est à dire la façon dont Java s'occupe de ces choses).

85voto

Jon Skeet Points 692016

Je n'ai pas vu cet effet - je n'ai certainement jamais rencontré il être un goulot d'étranglement.

Voici un très rugueux-et-prêt de référence qui montre (sur ma boîte de toute façon) délégués effectivement être plus rapide que les 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);
    }
}

Les résultats de la (.NET 3.5; .NET 4.0b2 est sur le même):

Interface: 5068
Delegate: 4404

Maintenant je n'ai pas la foi que cela signifie que les délégués sont vraiment plus rapide que les interfaces... mais ça me fait assez convaincu qu'ils ne sont pas d'un ordre de grandeur plus lent. En outre, cela fait presque rien à l'intérieur de la délégué/méthode de l'interface. Évidemment, le coût d'invocation va faire de moins en moins de différence que vous n'avez plus de travail par appel.

Une seule chose à faire attention est que vous n'êtes pas de la création d'un nouveau délégué à plusieurs reprises lorsque vous ne utilisez une interface unique instance. Cela pourrait causer un problème que ce serait provoquer la collecte des ordures etc. Si vous utilisez une méthode d'instance en tant que délégué à l'intérieur d'une boucle, vous trouverez qu'il est plus efficace de déclarer le délégué de la variable en dehors de la boucle, de créer une seule instance de délégué et de le 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 pourrait-il avoir été le problème que vous voir?

25voto

dsimcha Points 32831

Je trouve ça tout à fait invraisemblable qu'un délégué est sensiblement plus rapide ou plus lent que d'une méthode virtuelle. Si tout le délégué doit être très rapide. À un niveau inférieur, les délégués sont généralement mis en place quelque chose comme (à l'aide de C-style de la notation, mais s'il vous plaît pardonnez tout mineur erreurs de syntaxe que c'est juste une illustration):

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

Appel d'un délégué fonctionne quelque chose comme:

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

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

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

Pour appeler un vritual fonction, vous devez effectuer les opérations suivantes:

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 lors de l'utilisation de fonctions virtuelles vous passez par une couche supplémentaire d'indirection pour obtenir le pointeur de fonction. Cependant, cette indirection supplémentaire de la couche est souvent libre parce que les CPU de la branche prédicteurs deviner l'adresse du pointeur de fonction et éventuellement d'exécuter sa cible en parallèle avec l'adresse de la fonction. J'ai trouvé (bien que dans D, pas du C#), la quasi-appels de fonction dans une boucle serrée, ne sont plus lents que les non-incorporé des appels directs, à condition que pour chaque exécution de la boucle, elles sont toujours résoudre à la même fonction réelle.

24voto

Pete Montgomery Points 1755

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 .

7voto

Paulo Zemek Points 49

J'ai fait quelques tests (en .Net 3.5... plus tard, je vais vérifier à la maison à l'aide .Net 4). Le fait est: Obtenir un objet comme une interface, puis de l'exécution de la méthode est plus rapide que l'obtention d'un délégué d'une méthode, puis l'appel de la déléguer.

Compte tenu de la variable est déjà dans le type de droit (interface ou de son délégué) et de la simple invocation, il rend le délégué gagner.

Pour quelque raison, l'obtention d'un délégué sur une méthode d'interface (peut-être sur toute méthode virtuelle) est BEAUCOUP plus lente.

Et, considérant qu'il y a des cas, quand nous simple ne peut pas pré-stocker les délégué (comme dans les Dépêches, par exemple), qui peuvent justifier pourquoi les interfaces sont plus rapides.

Voici les résultats:

Pour obtenir de vrais résultats, le compiler en mode Release et l'exécuter en dehors de Visual Studio.

La vérification des appels directs à deux reprises
00:00:00.5834988
00:00:00.5997071

La vérification des appels d'interface, l'obtention de l'interface à chaque appel
00:00:05.8998212

La vérification des appels d'interface, l'obtention de l'interface une fois
00:00:05.3163224

Action de vérification (délégué) des appels, passer à l'action à chaque appel
00:00:17.1807980

Action de vérification (délégué) des appels, passer à l'Action une fois
00:00:05.3163224

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

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

Action de vérification (délégué) sur une méthode de l'interface, les deux fois
00:00:04.0036530

Comme vous pouvez le voir, les appels directs sont vraiment rapide. Le stockage de l'interface ou son délégué avant, et ensuite seulement l'appel, il est vraiment très rapide. Mais d'avoir à obtenir un délégué est plus lent que d'avoir à obtenir une interface. D'avoir à obtenir un délégué sur une méthode d'interface (ou de méthode virtuelle, pas sûr) est très lent (comparer les 5 secondes de l'obtention d'un objet comme une interface à la près de 4 minutes de faire de même pour obtenir l'action).

Le code qui a généré ces résultats c'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();
        }
    }

}

0voto

Dorian Yeager Points 11

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 cet aspect conteneur? Rien ne nous empêche, si d est un délégué, d’exécuter d + = d; ou en construisant 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é?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X