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 du dynamic
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:
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'essaie d'appeler une méthode simple sans opération de sept manières différentes:
- Appel de méthode direct
- En utilisant
dynamic
- Par réflexion
- Utilisation d'un
Action
qui a été précompilé à l'exécution (excluant ainsi le temps de compilation des résultats).
- Utiliser un
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)
- Utilisation d'une méthode générée dynamiquement qui est créée avant le test.
- Utilisation d'une méthode générée dynamiquement qui est instanciée paresseusement pendant le test.
Chacun est appelé 1 million de fois dans une boucle simple. Voici les résultats de chronométrage:
Direct: 3.4248ms
Dynamique: 45.0728ms
Réflexion: 888.4011ms Précompilé
: 21.9166ms
LazyCompiled: 30.2045ms
ILEmitted: 8.4918ms
LazyILEmitted: 14.3483ms
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 un dynamic
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éritables dynamic
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:
new TimedAction("Reflection, find method", () =>
{
typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
}),
new TimedAction("Reflection, predetermined method", () =>
{
method.Invoke(foo, args);
}),
new TimedAction("Reflection, create a delegate", () =>
{
((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
}),
new TimedAction("Reflection, cached delegate", () =>
{
methodDelegate.Invoke();
}),
... 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.