Update: Vorkompilierte und faul kompilierte Benchmarks hinzugefügt
Update 2: Es stellt sich heraus, ich liege falsch. Eine vollständige und korrekte Antwort finden Sie in Eric Lipperts Beitrag. Ich lasse dies hier wegen der Benchmark-Zahlen
* Update 3: IL-Emitted- und Lazy IL-Emitted-Benchmarks hinzugefügt, basierend auf Mark Gravells Antwort auf diese Frage .
Meines Wissens führt die Verwendung des dynamic
Schlüsselworts zur Laufzeit an und für sich nicht zu einer zusätzlichen Kompilierung (obwohl ich mir vorstellen kann, dass dies unter bestimmten Umständen möglich ist, je nachdem, welche Art von Objekten Ihre dynamischen Variablen unterstützen).
In Bezug auf die Leistung führt dynamic
dies von Natur aus zu einem gewissen Overhead, jedoch bei weitem nicht so viel, wie Sie vielleicht denken. Zum Beispiel habe ich gerade einen Benchmark erstellt, der so aussieht:
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>));
}
Wie Sie dem Code entnehmen können, versuche ich, eine einfache No-Op-Methode auf sieben verschiedene Arten aufzurufen:
- Direkter Methodenaufruf
- Verwenden von
dynamic
- Durch Reflexion
- Verwenden eines
Action
, das zur Laufzeit vorkompiliert wurde (wodurch die Kompilierungszeit von den Ergebnissen ausgeschlossen wird).
- Verwenden einer
Action
, die beim ersten Mal kompiliert wird, wenn sie benötigt wird, Verwenden einer nicht threadsicheren Lazy-Variablen (einschließlich Kompilierungszeit)
- Verwenden einer dynamisch generierten Methode, die vor dem Test erstellt wird.
- Verwenden einer dynamisch generierten Methode, die während des Tests träge instanziiert wird.
Jeder wird in einer einfachen Schleife 1 Million Mal aufgerufen. Hier sind die Timing-Ergebnisse:
Direkt: 3,4248 ms
Dynamisch: 45,0728 ms
Reflexion: 888,4011 ms
Vorkompiliert: 21,9166
ms
LazyCompiled: 30,2045
ms ILEmitted: 8,4918 ms LazyILEmitted: 14,3483 ms
Während die Verwendung des dynamic
Schlüsselworts eine Größenordnung länger dauert als der direkte Aufruf der Methode, gelingt es ihm dennoch, den Vorgang millionenfach in etwa 50 Millisekunden abzuschließen, was ihn weitaus schneller als die Reflexion macht. Wenn die von uns aufgerufene Methode versuchen würde, etwas Intensives zu tun, z. B. einige Zeichenfolgen miteinander zu kombinieren oder eine Sammlung nach einem Wert zu durchsuchen, würden diese Operationen wahrscheinlich den Unterschied zwischen einem direkten Aufruf und einem dynamic
Aufruf bei weitem überwiegen .
Die Leistung ist nur einer von vielen guten Gründen, sie nicht dynamic
unnötig zu verwenden. Wenn Sie jedoch mit echten dynamic
Daten arbeiten, kann sie Vorteile bieten, die die Nachteile bei weitem überwiegen.
Update 4
Basierend auf Johnbots Kommentar habe ich den Reflexionsbereich in vier separate Tests unterteilt:
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();
}),
... und hier sind die Benchmark-Ergebnisse:
Wenn Sie also eine bestimmte Methode vorgeben können, die Sie häufig aufrufen müssen, ist das Aufrufen eines zwischengespeicherten Delegaten, der auf diese Methode verweist, ungefähr so schnell wie das Aufrufen der Methode selbst. Wenn Sie jedoch festlegen müssen, welche Methode aufgerufen werden soll, während Sie sie aufrufen, ist das Erstellen eines Delegaten dafür sehr teuer.