J'ai une question sur les performances de dynamic
en C #. J'ai lu dynamic
que le compilateur s'exécute à nouveau, mais que fait-il?
Doit-il recompiler toute la méthode avec la dynamic
variable utilisée comme paramètre ou juste ces lignes avec un comportement / contexte dynamique?
J'ai remarqué que l'utilisation de dynamic
variables peut ralentir une simple boucle for de 2 ordres de grandeur.
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));
}
c#
performance
dynamic
Lukasz Madon
la source
la source
Réponses:
Voici l'affaire.
Pour chaque expression de votre programme qui est de type dynamique, le compilateur émet du code qui génère un "objet de site d'appel dynamique" unique qui représente l'opération. Ainsi, par exemple, si vous avez:
alors le compilateur générera du code qui est moralement comme ça. (Le code réel est un peu plus complexe; cela est simplifié à des fins de présentation.)
Voyez comment cela fonctionne jusqu'à présent? Nous générons le site d'appel une fois , peu importe le nombre de fois que vous appelez M. Le site d'appel vit pour toujours après l'avoir généré une fois. Le site d'appel est un objet qui représente "il va y avoir un appel dynamique à Foo ici".
OK, maintenant que vous avez le site d'appel, comment fonctionne l'invocation?
Le site d'appel fait partie du Dynamic Language Runtime. Le DLR dit "hmm, quelqu'un essaie de faire une invocation dynamique d'une méthode foo sur cet objet here. Est-ce que je sais quelque chose à ce sujet? Non. Alors je ferais mieux de le découvrir."
Le DLR interroge ensuite l'objet en d1 pour voir s'il s'agit de quelque chose de spécial. Peut-être s'agit-il d'un objet COM hérité, d'un objet Iron Python, d'un objet Iron Ruby ou d'un objet DOM IE. S'il ne s'agit pas de l'un de ces objets, il doit s'agir d'un objet C # ordinaire.
C'est le moment où le compilateur redémarre. Il n'y a pas besoin d'un lexer ou d'un analyseur, donc le DLR démarre une version spéciale du compilateur C # qui n'a que l'analyseur de métadonnées, l'analyseur sémantique pour les expressions et un émetteur qui émet des arbres d'expression au lieu de IL.
L'analyseur de métadonnées utilise Reflection pour déterminer le type de l'objet dans d1, puis le transmet à l'analyseur sémantique pour demander ce qui se passe lorsqu'un tel objet est appelé sur la méthode Foo. L'analyseur de résolution de surcharge comprend cela, puis construit un arbre d'expression - comme si vous aviez appelé Foo dans un arbre d'expression lambda - qui représente cet appel.
Le compilateur C # transmet ensuite cette arborescence d'expression au DLR avec une stratégie de cache. La politique est généralement "la deuxième fois que vous voyez un objet de ce type, vous pouvez réutiliser cet arbre d'expression plutôt que de me rappeler". Le DLR appelle ensuite Compile sur l'arborescence d'expression, qui appelle le compilateur expression-tree-to-IL et crache un bloc d'IL généré dynamiquement dans un délégué.
Le DLR met ensuite ce délégué en cache dans un cache associé à l'objet de site d'appel.
Ensuite, il appelle le délégué et l'appel Foo se produit.
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 du même type que la dernière fois, il récupère le délégué hors du cache et l'appelle. Si l'objet est d'un type différent, alors le cache manque et tout le processus recommence; nous effectuons une analyse sémantique de l'appel et stockons le résultat dans le cache.
Cela se produit pour chaque expression qui implique une dynamique. Donc par exemple si vous avez:
puis il y a trois sites d'appels dynamiques. Un pour l'appel dynamique à Foo, un pour l'ajout dynamique et un pour la conversion dynamique de dynamique à int. Chacun a sa propre analyse d'exécution et son propre cache de résultats d'analyse.
Ça a du sens?
la source
Mise à jour: Ajout de benchmarks précompilés et paresseux
Mise à jour 2: Il s'avère que je me trompe. Voir l'article d'Eric Lippert pour une réponse complète et correcte. Je laisse ça ici pour le bien des chiffres de référence
* Mise à jour 3: Ajout de benchmarks IL-Emitted et Lazy IL-Emitted, basés sur la réponse de Mark Gravell à cette question .
À ma connaissance, l'utilisation dudynamic
mot-clé ne provoque pas de compilation supplémentaire au moment de l'exécution en soi (même si j'imagine qu'il pourrait le faire dans des circonstances spécifiques, en fonction du type d'objets qui sauvegardent vos variables dynamiques).En ce qui concerne les performances,
dynamic
introduit intrinsèquement des frais généraux, mais pas autant que vous pourriez le penser. Par exemple, je viens de lancer un benchmark qui ressemble à ceci:Comme vous pouvez le voir dans le code, j'essaie d'appeler une méthode simple sans opération de sept manières différentes:
dynamic
Action
qui a été précompilé à l'exécution (excluant ainsi le temps de compilation des résultats).Action
qui est compilé la première fois que cela est nécessaire, en utilisant une variable Lazy non thread-safe (incluant ainsi le temps de compilation)Chacun est appelé 1 million de fois dans une boucle simple. Voici les résultats de chronométrage:
Ainsi, même si l'utilisation du
dynamic
mot - clé prend un ordre de grandeur plus long que d'appeler directement la méthode, elle parvient toujours à terminer l'opération un million de fois en environ 50 millisecondes, ce qui la rend beaucoup plus rapide que la réflexion. Si la méthode que nous appelons essayait de faire quelque chose d'intensif, comme combiner quelques chaînes ensemble ou rechercher une collection pour une valeur, ces opérations l'emporteraient probablement de loin sur la différence entre un appel direct et undynamic
appel.Les performances ne sont que l'une des nombreuses bonnes raisons de ne pas utiliser
dynamic
inutilement, mais lorsque vous traitez avec de véritablesdynamic
données, elles peuvent offrir des avantages qui l'emportent largement sur les inconvénients.Mise à jour 4
Sur la base du commentaire de Johnbot, j'ai divisé la zone de réflexion en quatre tests distincts:
... et voici les résultats de référence:
Donc, si vous pouvez prédéterminer une méthode spécifique que vous devrez appeler beaucoup, invoquer un délégué mis en cache faisant référence à cette méthode est à peu près aussi rapide que d'appeler la méthode elle-même. Cependant, si vous avez besoin de déterminer la méthode à appeler au moment où vous êtes sur le point de l'invoquer, la création d'un délégué pour elle est très coûteuse.
la source
dynamic
bien sûr une défaite:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);