c# - Rendimiento de Expression.Compile vs Lambda, llamadas directas vs virtuales
performance virtual-method (3)
Tengo curiosidad por saber cuál es el rendimiento de Expression.Compile comparación con la expresión lambda en el código y en comparación con el uso directo de métodos, y también las llamadas a métodos directos y las llamadas a métodos virtuales (pseudo código):
var foo = new Foo();
var iFoo = (IFoo)foo;
foo.Bar();
iFoo.Bar();
(() => foo.Bar())();
(() => iFoo.Bar())();
Expression.Compile(foo, Foo.Bar)();
Expression.Compile(iFoo, IFoo.Bar)();
Expression.CompileToMethod(foo, Foo.Bar);
Expression.CompileToMethod(iFoo, IFoo.Bar);
MethodInfo.Invoke(foo, Foo.Bar);
MethodInfo.Invoke(iFoo, IFoo.Bar);
Consejo: en el modo de liberación no se realiza ninguna llamada en el caso de "Llamada directa". La CPU va de 00B531BC ( mov eax ... ) a 00B531C8 ( jl 00B531BC ) solamente.
for (int i = 0; i < iterationCount; i++)
00B531BA xor edx,edx
foo.Bar();
00B531BC mov eax,dword ptr [ebx+4] // actual loop begin
00B531BF cmp byte ptr [eax],al
for (int i = 0; i < iterationCount; i++)
00B531C1 inc edx
00B531C2 cmp edx,0BEBC200h // 0BEBC200h = 200000000
00B531C8 jl 00B531BC // loop begin address
No encontré ninguna respuesta, así que aquí está la prueba de rendimiento:
using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
namespace ExpressionTest
{
public interface IFoo
{
int Bar();
}
public sealed class FooImpl : IFoo
{
public int Bar()
{
return 0;
}
}
class Program
{
static void Main(string[] args)
{
var foo = new FooImpl();
var iFoo = (IFoo)foo;
Func<int> directLambda = () => foo.Bar();
Func<int> virtualLambda = () => iFoo.Bar();
var compiledDirectCall = CompileBar(foo, asInterfaceCall: false);
var compiledVirtualCall = CompileBar(foo, asInterfaceCall: true);
var compiledArgDirectCall = CompileBar<FooImpl>();
var compiledArgVirtualCall = CompileBar<IFoo>();
var barMethodInfo = typeof(FooImpl).GetMethod(nameof(FooImpl.Bar));
var iBarMethodInfo = typeof(IFoo).GetMethod(nameof(IFoo.Bar));
var compiledToModuleDirect = CompileToModule<FooImpl>();
var compiledToModuleVirtual = CompileToModule<IFoo>();
var iterationCount = 200000000;
Console.WriteLine($"Iteration count: {iterationCount:N0}");
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterationCount; i++)
compiledVirtualCall();
var elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Virtual (Func<int>)Expression.Compile(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
compiledDirectCall();
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Direct (Func<int>)Expression.Compile(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
compiledArgVirtualCall(iFoo);
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Virtual (Func<IFoo, int>)Expression.Compile(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
compiledArgDirectCall(foo);
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Direct (Func<FooImpl, int>)Expression.Compile(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
compiledToModuleVirtual(iFoo);
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Virtual (Func<IFoo, int>)Expression.CompileToMethod(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
compiledToModuleDirect(foo);
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Direct (Func<FooImpl, int>)Expression.CompileToMethod(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
virtualLambda();
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Virtual () => IFoo.Bar(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
directLambda();
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Direct () => FooImpl.Bar(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
iFoo.Bar();
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Virtual IFoo.Bar(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++)
foo.Bar();
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Direct Foo.Bar(): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++) {
int result = (int)iBarMethodInfo.Invoke(iFoo, null);
}
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Virtual MethodInfo.Invoke(FooImpl, Bar): {elapsedMs} ms");
sw.Restart();
for (int i = 0; i < iterationCount; i++) {
int result = (int)barMethodInfo.Invoke(foo, null);
}
elapsedMs = sw.ElapsedMilliseconds;
Console.WriteLine($"Direct MethodInfo.Invoke(IFoo, Bar): {elapsedMs} ms");
}
static Func<int> CompileBar(IFoo foo, bool asInterfaceCall)
{
var fooType = asInterfaceCall ? typeof(IFoo) : foo.GetType();
var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
var instance = Expression.Constant(foo, fooType);
var call = Expression.Call(instance, methodInfo);
var lambda = Expression.Lambda(call);
var compiledFunction = (Func<int>)lambda.Compile();
return compiledFunction;
}
static Func<TInput, int> CompileBar<TInput>()
{
var fooType = typeof(TInput);
var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
var instance = Expression.Parameter(fooType, "foo");
var call = Expression.Call(instance, methodInfo);
var lambda = Expression.Lambda(call, instance);
var compiledFunction = (Func<TInput, int>)lambda.Compile();
return compiledFunction;
}
static Func<TInput, int> CompileToModule<TInput>()
{
var fooType = typeof(TInput);
var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
var instance = Expression.Parameter(fooType, "foo");
var call = Expression.Call(instance, methodInfo);
var lambda = Expression.Lambda(call, instance);
var asmName = new AssemblyName(fooType.Name);
var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var moduleBuilder = asmBuilder.DefineDynamicModule(fooType.Name);
var typeBuilder = moduleBuilder.DefineType(fooType.Name, TypeAttributes.Public);
var methodBuilder = typeBuilder.DefineMethod(nameof(IFoo.Bar), MethodAttributes.Static, typeof(int), new[] { fooType });
Expression.Lambda<Action>(lambda).CompileToMethod(methodBuilder);
var createdType = typeBuilder.CreateType();
var mi = createdType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)[1];
var func = Delegate.CreateDelegate(typeof(Func<TInput, int>), mi);
return (Func<TInput, int>)func;
}
}
}
En mi computadora portátil (modo de lanzamiento, 64 bits, .NET 4.5.2) produce:
Iteration count: 200,000,000
Virtual MethodInfo.Invoke(FooImpl, Bar): 61811 ms
Direct MethodInfo.Invoke(IFoo, Bar): 37078 ms
Virtual (Func<int>)Expression.Compile(): 2894 ms
Direct (Func<int>)Expression.Compile(): 2242 ms
Virtual (Func<IFoo, int>)Expression.Compile(): 2319 ms
Direct (Func<FooImpl, int>)Expression.Compile(): 2051 ms
Virtual (Func<IFoo, int>)Expression.CompileToMethod(): 996 ms
Direct (Func<FooImpl, int>)Expression.CompileToMethod(): 679 ms
Virtual () => IFoo.Bar(): 796 ms
Direct () => FooImpl.Bar(): 469 ms
Virtual IFoo.Bar(): 531 ms
Direct Foo.Bar(): 68 ms
Espero que esto ayude.
Podemos dividir una pregunta en 2 casos:
- ¿Cómo funciona .NET con el método que se llama a sí mismo (pregunta de infraestructura)?
- ¿Cómo los optimizadores ayudan al método de llamada?
ExpressionTest.exe en modo de lanzamiento con optimización (configuración de lanzamiento predeterminada) .NET 4.5.2:
Compiled Virtual Call: 4625 ms
Compiled Direct Call: 3361 ms
Lambda Virtual Call: 1096 ms
Lambda Direct Call: 576 ms
Virtual Call: 649 ms
Direct Call: 144 ms
Vemos que "Direct Call" en 4.5 veces más rápido que "Virtual Call". Pero como vemos arriba, no hay ninguna llamada. El método de barra estaba en línea.
ExpressionTest.exe en modo de lanzamiento sin optimización .NET 4.5.2:
Compiled Virtual Call: 5394 ms
Compiled Direct Call: 4666 ms
Lambda Virtual Call: 1800 ms
Lambda Direct Call: 1683 ms
Virtual Call: 1154 ms
Direct Call: 1112 ms
Por lo tanto, "Direct Call" es aproximadamente un 3-4% más rápido que "Virtual Call".
Pregunta similar: rendimiento de la llamada virtual "directa" frente a la llamada de interfaz en C #