Aggiornamento: aggiunti benchmark precompilati e compilati in modo pigro
Aggiornamento 2: risulta, mi sbaglio. Vedi il post di Eric Lippert per una risposta completa e corretta. Lascio questo qui per il bene dei numeri di riferimento
* Aggiornamento 3: aggiunti benchmark IL-Emitted e Lazy IL-Emitted, basati sulla risposta di Mark Gravell a questa domanda .
Per quanto ne sappia, l'uso della dynamic
parola chiave non causa alcuna compilazione aggiuntiva in fase di esecuzione in sé e per sé (anche se immagino che potrebbe farlo in circostanze specifiche, a seconda del tipo di oggetti che supportano le variabili dinamiche).
Per quanto riguarda le prestazioni, dynamic
introduce intrinsecamente un certo sovraccarico, ma non tanto quanto si potrebbe pensare. Ad esempio, ho appena eseguito un benchmark simile al seguente:
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>));
}
Come puoi vedere dal codice, provo a invocare un semplice metodo no-op in sette modi diversi:
- Chiamata diretta al metodo
- utilizzando
dynamic
- Per riflessione
- Utilizzo di uno
Action
che è stato precompilato in fase di esecuzione (escludendo così i tempi di compilazione dai risultati).
- Usare un file
Action
che viene compilato la prima volta che è necessario, usando una variabile Lazy non thread-safe (includendo quindi il tempo di compilazione)
- Utilizzando un metodo generato dinamicamente che viene creato prima del test.
- Utilizzando un metodo generato dinamicamente che viene pigramente istanziato durante il test.
Ciascuno viene chiamato 1 milione di volte in un semplice ciclo. Ecco i risultati dei tempi:
Diretto: 3,4248 ms
Dinamico: 45,0728 ms
Riflessione: 888,4011
ms
Precompilato: 21,9166
ms Pigro
Compilato: 30,2045 ms ILEmmesso: 8,4918 ms LazyILEmesso: 14,3483 ms
Quindi, mentre l'utilizzo della dynamic
parola chiave richiede un ordine di grandezza più lungo rispetto alla chiamata diretta del metodo, riesce comunque a completare l'operazione un milione di volte in circa 50 millisecondi, rendendolo molto più veloce della riflessione. Se il metodo che chiamiamo cercasse di fare qualcosa di intensivo, come combinare alcune stringhe insieme o cercare un valore in una raccolta, quelle operazioni probabilmente supererebbero di gran lunga la differenza tra una chiamata diretta e una dynamic
chiamata.
Le prestazioni sono solo una delle molte buone ragioni per non usarle dynamic
inutilmente, ma quando si ha a che fare con dynamic
dati reali , possono fornire vantaggi che superano di gran lunga gli svantaggi.
Aggiornamento 4
Sulla base del commento di Johnbot, ho suddiviso l'area Reflection in quattro test separati:
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();
}),
... ed ecco i risultati del benchmark:
Quindi, se riesci a predeterminare un metodo specifico che dovrai chiamare molto, invocare un delegato memorizzato nella cache facendo riferimento a quel metodo è veloce quanto chiamare il metodo stesso. Tuttavia, se è necessario determinare quale metodo chiamare mentre si sta per invocarlo, la creazione di un delegato è molto costosa.