Actualización: Se agregaron puntos de referencia precompilados y compilados de manera diferida
Actualización 2: Resulta que estoy equivocado. Vea la publicación de Eric Lippert para obtener una respuesta completa y correcta. Dejo esto aquí por el bien de los números de referencia
* Actualización 3: Se agregaron puntos de referencia IL-Emitted y Lazy IL-Emitted, según la respuesta de Mark Gravell a esta pregunta .
Que yo sepa, el uso de la dynamic
palabra clave no causa ninguna compilación adicional en tiempo de ejecución en sí misma (aunque imagino que podría hacerlo en circunstancias específicas, dependiendo de qué tipo de objetos respaldan sus variables dinámicas).
Con respecto al rendimiento, dynamic
inherentemente introduce algo de sobrecarga, pero no tanto como podría pensar. Por ejemplo, acabo de ejecutar un punto de referencia que se ve así:
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>));
}
Como puede ver en el código, trato de invocar un método simple sin operación de siete maneras diferentes:
- Llamada al método directo
- Utilizando
dynamic
- Por reflexión
- Usando un
Action
precompilado en tiempo de ejecución (excluyendo así el tiempo de compilación de los resultados).
- Usando una
Action
que se compila la primera vez que se necesita, usando una variable Lazy no segura para subprocesos (incluyendo el tiempo de compilación)
- Usando un método generado dinámicamente que se crea antes de la prueba.
- Usando un método generado dinámicamente que se instancia perezosamente durante la prueba.
Cada uno se llama 1 millón de veces en un bucle simple. Aquí están los resultados de tiempo:
Directo: 3.4248ms
Dinámico: 45.0728ms
Reflexión: 888.4011ms
Precompilado: 21.9166ms
LazyCompiled: 30.2045ms
ILEmitted: 8.4918ms
LazyILEmitmit: 14.3483ms
Entonces, aunque usar la dynamic
palabra clave toma un orden de magnitud más largo que llamar al método directamente, aún logra completar la operación un millón de veces en aproximadamente 50 milisegundos, por lo que es mucho más rápido que la reflexión. Si el método que llamamos intentara hacer algo intensivo, como combinar algunas cadenas o buscar un valor en una colección, esas operaciones probablemente superarían con creces la diferencia entre una llamada directa y una dynamic
llamada.
El rendimiento es solo una de las muchas buenas razones para no usar dynamic
innecesariamente, pero cuando se trata de dynamic
datos reales , puede proporcionar ventajas que superan con creces las desventajas.
Actualización 4
Basado en el comentario de Johnbot, dividí el área de Reflection en cuatro pruebas separadas:
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();
}),
... y aquí están los resultados de referencia:
Entonces, si puede predeterminar un método específico que necesitará llamar mucho, invocar a un delegado en caché que se refiera a ese método es tan rápido como llamar al método en sí. Sin embargo, si necesita determinar a qué método llamar justo cuando está a punto de invocarlo, crear un delegado es muy costoso.