Atualização: Adicionados benchmarks pré-compilados e preguiçosos
Atualização 2: Acontece que eu estou errado. Veja a publicação de Eric Lippert para obter uma resposta completa e correta. Estou deixando isso aqui por causa dos números de referência
* Atualização 3: Adicionados parâmetros de referência emitidos por IL e preguiçosos emitidos por IL, com base na resposta de Mark Gravell a esta pergunta .
Que eu saiba, o uso da dynamic
palavra - chave não causa nenhuma compilação extra em tempo de execução por si só (embora eu imagine que poderia fazê-lo em circunstâncias específicas, dependendo do tipo de objeto que está apoiando suas variáveis dinâmicas).
Em relação ao desempenho, dynamic
introduz inerentemente alguma sobrecarga, mas não tanto quanto você imagina. Por exemplo, eu apenas executei uma referência parecida com esta:
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 você pode ver no código, tento invocar um método simples não operacional de sete maneiras diferentes:
- Chamada de método direta
- Usando
dynamic
- Pela reflexão
- Usando um
Action
que foi pré-compilado em tempo de execução (excluindo assim o tempo de compilação dos resultados).
- Usando um
Action
que é compilado na primeira vez em que é necessário, usando uma variável Lazy não segura para thread (incluindo o tempo de compilação)
- Usando um método gerado dinamicamente que é criado antes do teste.
- Usando um método gerado dinamicamente que é instanciado preguiçosamente durante o teste.
Cada um é chamado 1 milhão de vezes em um loop simples. Aqui estão os resultados do tempo:
Direto: 3.4248ms
Dinâmico: 45.0728ms
Reflexo: 888.4011ms
Pré-compilado: 21.9166ms
PreguiçosoCompilado: 30.2045ms
ILEmitido: 8.4918ms
PreguiçosoILEmitido: 14.3483ms
Portanto, enquanto o uso da dynamic
palavra-chave leva uma ordem de magnitude maior que a chamada direta do método, ele ainda consegue concluir a operação um milhão de vezes em cerca de 50 milissegundos, tornando-a muito mais rápida que a reflexão. Se o método que chamamos estava tentando fazer algo intensivo, como combinar algumas cadeias ou procurar um valor em uma coleção, essas operações provavelmente superariam em muito a diferença entre uma chamada direta e uma dynamic
chamada.
O desempenho é apenas uma das muitas boas razões para não usar dynamic
desnecessariamente, mas quando você lida com dynamic
dados reais , pode oferecer vantagens que superam as desvantagens.
Atualização 4
Baseado no comentário de Johnbot, dividi a área de reflexão em quatro testes separados:
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();
}),
... e aqui estão os resultados de referência:
Portanto, se você pode predeterminar um método específico que precisará chamar muito, chamar um delegado em cache que se refere a esse método é tão rápido quanto chamar o próprio método. No entanto, se você precisar determinar qual método chamar, assim que estiver prestes a invocá-lo, a criação de um representante para ele será muito cara.