139 votes

dynamique et performance

J'ai une question sur les performances de la dynamique en C #. J'ai lu dynamic fait fonctionner le compilateur à nouveau, mais ce qu'il fait. Doit-il recompiler la méthode entière avec la dynamique utilisée en tant que paramètre ou plutôt ces lignes avec comportement dynamique / contexte (?)

J'ai remarqué que l'utilisation de variables dynamiques peut ralentir une boucle simple de 2 ordres de grandeur.

EDIT: Code avec lequel j'ai joué

 internal class Sum2
{
    public int intSum;
}

internal class Sum
{
    public dynamic DynSum;
    public int intSum;
}

class Program
{
    private const int ITERATIONS = 1000000;

    static void Main(string[] args)
    {
        var stopwatch = new Stopwatch();
        dynamic param = new Object();
        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        DynamicSum(stopwatch);
        SumInt(stopwatch);
        SumInt(stopwatch, param);
        Sum(stopwatch);

        Console.ReadKey();

    }

    private static void Sum(Stopwatch stopwatch)
    {
        var sum = 0;
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }

    private static void SumInt(Stopwatch stopwatch, dynamic param)
    {
        var sum = new Sum2();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.intSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
    }

    private static void DynamicSum(Stopwatch stopwatch)
    {
        var sum = new Sum();
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < ITERATIONS; i++)
        {
            sum.DynSum += i;
        }
        stopwatch.Stop();

        Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
    }
 

247voto

Eric Lippert Points 300275

J'ai lu dynamique de fait le compilateur, mais ce qu'il fait. Il faut recompiler toute la méthode de la dynamique utilisé comme un paramètre ou plutôt de ces lignes avec un comportement dynamique/contexte(?)

Voici l'affaire.

Pour chaque expression dans votre programme qui est de type dynamique, le compilateur émet un code qui génère un seul "dynamique de l'appel objet de site" qui représente l'opération. Ainsi, par exemple, si vous avez:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

ensuite, le compilateur génère un code qui est moralement comme ça. (Le code est un peu plus complexe; c'est simplifiée pour les besoins de la présentation.)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

Voir comment cela fonctionne jusqu'à présent? Nous générons le site d'appel une fois, peu importe combien de fois vous appeler M. L'appel site vit pour toujours, d'après vous générez une fois. Le site d'appel est un objet qui représente "il va y avoir un appel dynamique de Foo ici".

OK, alors maintenant que vous avez reçu l'appel site, comment l'invocation de travail?

Le site d'appel est la partie de la Dynamique de la Langue de l'Exécution. La RDL dit: "hmm, quelqu'un tente de se faire une dynamique de l'invocation d'une méthode foo sur ce ici de l'objet. Je ne sais rien à ce sujet? Pas de. Alors je ferais mieux de le savoir."

Le DLR, puis interroge l'objet en d1 pour voir si c'est quelque chose de spécial. Peut-être que c'est un héritage de l'objet COM, ou un Fer à repasser objet Python, ou un Fer à repasser Ruby objet, ou un IE objet DOM. Si ce n'est pas tout, alors il doit être un banal objet de C#.

C'est le point où le compilateur commence de nouveau. Il n'y a pas besoin d'un analyseur lexical ou de l'analyseur, de sorte que le DLR lance une version spéciale du compilateur C# qui a juste les métadonnées de l'analyseur l'analyseur sémantique des expressions, et d'un émetteur qui émet des Arbres d'Expression au lieu de IL.

Les métadonnées de l'analyseur utilise la Réflexion pour déterminer le type de l'objet en d1, puis passe à la sémantique de l'analyseur pour demander ce qui se produit lorsqu'un objet est appelée sur la méthode Foo. La résolution de surcharge de l'analyseur de chiffres, puis construit une Arborescence d'Expression, un peu comme si vous aviez appelé Foo dans une arborescence d'expression lambda -- qui représente à cet appel.

Le compilateur C# passe alors que l'expression de l'arbre de retour à la DLR avec une politique de cache. La politique est généralement "la deuxième fois que vous voyez un objet de ce type, vous pouvez ré-utiliser cette expression de l'arbre plutôt que de m'appeler de nouveau de retour". Le DLR appelle ensuite Compiler sur l'expression de l'arbre, qui invoque l'expression-l'arbre-à-l'IL du compilateur et crache un bloc d'générées dynamiquement IL dans un délégué.

Le DLR met ensuite ce délégué dans un cache associée à l'appel objet du site.

Ensuite, elle appelle le délégué, et les Foo appel arrive.

La deuxième fois que vous appelez M, nous avons déjà un site d'appel. Le DLR interroge à nouveau l'objet, et si l'objet est le même type que c'était la dernière fois, il récupère le délégué de la mémoire cache et l'appelle. Si l'objet est d'un type différent, puis le cache, et tout le processus recommence de nouveau; nous faisons l'analyse sémantique de l'appel et de stocker le résultat dans le cache.

Ce qui se passe pour chaque expression qui implique dynamique. Ainsi, par exemple, si vous avez:

int x = d1.Foo() + d2;

ensuite, il y a trois dynamiques d'appels de sites. Un pour la dynamique de l'appel à Toto, un pour le plus dynamique, et un pour la conversion dynamique de dynamique à l'int. Chacun a sa propre analyse de l'exécution et de son propre cache de résultats d'analyse.

Un sens?

112voto

StriplingWarrior Points 56276

Mise à jour: Ajout de précompilés et paresseux-compilé repères

Mise à jour 2: s'avère que j'ai tort. Voir Eric Lippert post complet et de bonne réponse. Je pars de ce ici pour l'amour de l'indice de référence des numéros de

*Mise à jour 3: Ajout d'IL-Émis et Paresseux IL Émis des repères, basé sur Marc Gravel, la réponse à cette question.

À ma connaissance, l'utilisation de l' dynamic mot clé n'a pas de cause supplémentaire de la compilation à l'exécution dans et de lui-même (bien que j'imagine qu'il pourrait le faire dans des circonstances spécifiques, en fonction de ce type d'objets sont la sauvegarde de vos variables dynamiques).

Concernant le rendement, dynamic ne intrinsèquement introduire une surcharge, mais pas autant que vous pourriez le penser. Par exemple, j'ai juste couru un point de repère qui ressemble à ceci:

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

Comme vous pouvez le voir dans le code, j'ai essayer d'appeler une méthode no-op simple sept manières différentes:

  1. Direct appel de la méthode
  2. À l'aide de dynamic
  3. Par la réflexion
  4. À l'aide d'un Action qui a obtenu précompilés au moment de l'exécution (excluant donc les temps de compilation des résultats).
  5. À l'aide d'un Action qui sera compilé la première fois qu'il est nécessaire, à l'aide d'un non thread-safe Paresseux variable (y compris, donc, le temps de compilation)
  6. À l'aide d'un générées dynamiquement la méthode qui vient d'être créé avant le test.
  7. À l'aide d'un générées dynamiquement méthode qui est paresseusement instancié lors de l'essai.

Chacun est appelé à 1 million de fois en une simple boucle. Voici le calendrier résultats:

Direct: 3.4248 ms
Dynamique: 45.0728 ms
Réflexion: 888.4011 ms
Précompilés: 21.9166 ms
LazyCompiled: 30.2045 ms
ILEmitted: 8.4918 ms
LazyILEmitted: 14.3483 ms

Ainsi, alors que l'aide de l' dynamic mot-clé prend un ordre de grandeur plus que l'appel de la méthode directe, elle parvient toujours à terminer l'opération d'un million de fois en 50 millisecondes, ce qui rend beaucoup plus rapide que de réflexion. Si la méthode que nous appelons ont essayé de faire quelque chose d'intensité, comme la combinaison d'un peu de chaînes ou à la recherche d'une collection, d'une valeur, ces opérations seraient probablement l'emportent de loin sur la différence entre un appel direct et un dynamic appel.

La Performance est l'une des nombreuses bonnes raisons de ne pas utiliser dynamic inutilement, mais lorsque vous faites affaire avec vraiment dynamic de données, il peut offrir des avantages qui l'emportent sur les inconvénients.

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