example c# delegates expression expression-trees func

c# - example - linq select



¿Por qué Func<> creado desde Expression<Func<>> más lento que Func<> declarado directamente? (6)

¿Por qué un Func<> creado a partir de una Expression<Func<>> través de .Compile () considerablemente más lento que el simple uso de un Func<> declarado directamente?

Acabo de cambiar de usar un Func<IInterface, object> declarado directamente a uno creado a partir de una Expression<Func<IInterface, object>> en una aplicación en la que estoy trabajando y noté que el rendimiento disminuyó.

Acabo de hacer una pequeña prueba, y el Func<> creado a partir de una Expresión toma "casi" el doble de tiempo que un Func<> declarado directamente.

En mi máquina, Direct Func<> tarda unos 7,5 segundos y Expression<Func<>> tarda unos 12,6 segundos.

Aquí está el código de prueba que utilicé (ejecutando Net 4.0)

// Direct Func<int, Foo> test1 = x => new Foo(x * 2); int counter1 = 0; Stopwatch s1 = new Stopwatch(); s1.Start(); for (int i = 0; i < 300000000; i++) { counter1 += test1(i).Value; } s1.Stop(); var result1 = s1.Elapsed; // Expression . Compile() Expression<Func<int, Foo>> expression = x => new Foo(x * 2); Func<int, Foo> test2 = expression.Compile(); int counter2 = 0; Stopwatch s2 = new Stopwatch(); s2.Start(); for (int i = 0; i < 300000000; i++) { counter2 += test2(i).Value; } s2.Stop(); var result2 = s2.Elapsed; public class Foo { public Foo(int i) { Value = i; } public int Value { get; set; } }

¿Cómo puedo recuperar el rendimiento?

¿Hay algo que pueda hacer para que el Func<> creado desde Expression<Func<>> funcione como uno declarado directamente?


(Esta no es una respuesta adecuada, pero es material destinado a ayudar a descubrir la respuesta).

Estadísticas recopiladas de Mono 2.6.7 - Debian Lenny - Linux 2.6.26 i686 - 2.80GHz single core:

Func: 00:00:23.6062578 Expression: 00:00:23.9766248

Entonces, en Mono, al menos, ambos mecanismos parecen generar IL equivalente.

Esta es la IL generada por las gmcs de Mono para el método anónimo:

// method line 6 .method private static hidebysig default class Foo ''<Main>m__0'' (int32 x) cil managed { .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::''.ctor''() = (01 00 00 00 ) // .... // Method begins at RVA 0x2204 // Code size 9 (0x9) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldc.i4.2 IL_0002: mul IL_0003: newobj instance void class Foo::''.ctor''(int32) IL_0008: ret } // end of method Default::<Main>m__0

Trabajaré en extraer el IL generado por el compilador de expresiones.


Como han mencionado otros, la sobrecarga de llamar a un delegado dinámico está causando su desaceleración. En mi computadora esa sobrecarga es de aproximadamente 12ns con mi CPU a 3GHz. La forma de evitar esto es cargar el método desde un ensamblado compilado, como este:

var ab = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("assembly"), AssemblyBuilderAccess.Run); var mod = ab.DefineDynamicModule("module"); var tb = mod.DefineType("type", TypeAttributes.Public); var mb = tb.DefineMethod( "test3", MethodAttributes.Public | MethodAttributes.Static); expression.CompileToMethod(mb); var t = tb.CreateType(); var test3 = (Func<int, Foo>)Delegate.CreateDelegate( typeof(Func<int, Foo>), t.GetMethod("test3")); int counter3 = 0; Stopwatch s3 = new Stopwatch(); s3.Start(); for (int i = 0; i < 300000000; i++) { counter3 += test3(i).Value; } s3.Stop(); var result3 = s3.Elapsed;

Cuando agrego el código anterior, result3 siempre es solo una fracción de segundo más alto que result1 , por alrededor de 1ns de sobrecarga.

Entonces, ¿por qué molestarse con un lambda compilado ( test2 ) cuando puede tener un delegado más rápido ( test3 )? Debido a que la creación del ensamblaje dinámico es mucho más general en general, y solo le ahorra 10-20ns en cada invocación.


Estaba interesado en la respuesta de Michael B. Entonces agregué en cada caso una llamada adicional antes de que comenzara el cronómetro. En el modo de depuración, el método de compilación (caso 2) fue más rápido casi dos veces (6 segundos a 10 segundos), y en el modo de lanzamiento en ambas versiones ambas versiones estaban a la par (la diferencia era aproximadamente ~ 0.2 segundos).

Ahora, lo que es sorprendente para mí, que con JIT fuera de la ecuación obtuve los resultados opuestos a los de Martin.

Editar: Inicialmente me perdí el Foo, por lo que los resultados anteriores son para Foo con campo, no una propiedad, con Foo original la comparación es la misma, solo que las veces son más grandes: 15 segundos para el func directo, 12 segundos para la versión compilada. De nuevo, en el modo de lanzamiento los tiempos son similares, ahora la diferencia es aproximadamente ~ 0.5.

Sin embargo, esto indica que si su expresión es más compleja, incluso en el modo de lanzamiento habrá una diferencia real.


Solo para el registro: puedo reproducir los números con el código de arriba.

Una cosa a tener en cuenta es que ambos delegados crean una nueva instancia de Foo para cada iteración. Esto podría ser más importante que la forma en que se crean los delegados. Esto no solo genera muchas asignaciones de montón, sino que GC también puede afectar los números aquí.

Si cambio el código a

Func<int, int> test1 = x => x * 2;

y

Expression<Func<int, int>> expression = x => x * 2; Func<int, int> test2 = expression.Compile();

Las cifras de rendimiento son prácticamente idénticas (en realidad, el resultado2 es un poco mejor que el resultado1). Esto respalda la teoría de que la parte costosa son asignaciones de fondos y / o cobros y no cómo se construye el delegado.

ACTUALIZAR

Siguiendo el comentario de Gabe, intenté cambiar a Foo para que fuera una estructura. Desafortunadamente, esto produce más o menos los mismos números que el código original, por lo que quizás la asignación de montón / recolección de basura no sea la causa después de todo.

Sin embargo, también verifiqué los números para delegados del tipo Func<int, int> y son bastante similares y mucho más bajos que los números del código original.

Seguiré cavando y espero ver más / respuestas actualizadas.


En última instancia, todo se reduce a que Expression<T> no es un delegado pre compilado. Es solo un árbol de expresiones. Llamar a Compile en LambdaExpression (que es lo que realmente es Expression<T> ) genera código IL en tiempo de ejecución y crea algo parecido a DynamicMethod para él.

Si solo usa un Func<T> en el código, lo compila previamente igual que cualquier otra referencia de delegado.

Entonces, hay 2 fuentes de lentitud aquí:

  1. El tiempo de compilación inicial para compilar Expression<T> en un delegado. Esto es enorme Si está haciendo esto para cada invocación, definitivamente no (pero este no es el caso, ya que está usando su Cronómetro después de llamar a la compilación).

  2. Es un DynamicMethod básicamente después de llamar a Compile. DynamicMethod s (incluso los delegados fuertemente tipados para unos) SON de hecho más lentos en ejecutar que las llamadas directas. Func<T> s resueltos en el momento de la compilación son llamadas directas. Hay comparaciones de rendimiento entre IL emitidas dinámicamente y IL de compilación en tiempo emitido. URL aleatoria: http://www.codeproject.com/KB/cs/dynamicmethoddelegates.aspx?msg=1160046

... Además, en su prueba de cronómetro para Expression<T> , debe iniciar su temporizador cuando i = 1, no 0 ... Creo que su Lambda compilado no se compilará JIT hasta la primera invocación, por lo que habrá un golpe de rendimiento para esa primera llamada.


Lo más probable es que la primera invocación del código no se juntó. Decidí mirar el IL y son prácticamente idénticos.

Func<int, Foo> func = x => new Foo(x * 2); Expression<Func<int, Foo>> exp = x => new Foo(x * 2); var func2 = exp.Compile(); Array.ForEach(func.Method.GetMethodBody().GetILAsByteArray(), b => Console.WriteLine(b)); var mtype = func2.Method.GetType(); var fiOwner = mtype.GetField("m_owner", BindingFlags.Instance | BindingFlags.NonPublic); var dynMethod = fiOwner.GetValue(func2.Method) as DynamicMethod; var ilgen = dynMethod.GetILGenerator(); byte[] il = ilgen.GetType().GetMethod("BakeByteArray", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ilgen, null) as byte[]; Console.WriteLine("Expression version"); Array.ForEach(il, b => Console.WriteLine(b));

Este código nos proporciona los arrays de bytes y los imprime en la consola. Aquí está la salida en mi máquina ::

2 24 90 115 13 0 0 6 42 Expression version 3 24 90 115 2 0 0 6 42

Y aquí está la versión reflectora de la primera función ::

L_0000: ldarg.0 L_0001: ldc.i4.2 L_0002: mul L_0003: newobj instance void ConsoleApplication7.Foo::.ctor(int32) L_0008: ret

¡Hay solo 2 bytes diferentes en todo el método! Son el primer código de operación, que es para el primer método, ldarg0 (carga el primer argumento), pero en el segundo método ldarg1 (carga el segundo argumento). La diferencia aquí es porque un objeto generado por expresión en realidad tiene un objetivo de un objeto Closure . Esto también puede tener en cuenta.

El siguiente código de operación para ambos es ldc.i4.2 (24) que significa carga 2 en la pila, el siguiente es el código de operación para mul (90), el siguiente código de operación es el newobj operación newobj (115). Los siguientes 4 bytes son el token de metadatos para el objeto .ctor . Son diferentes ya que los dos métodos están alojados en diferentes conjuntos. El método anónimo está en una asamblea anónima. Lamentablemente, no he llegado al punto de averiguar cómo resolver estos tokens. El código de operación final es 42, que es ret . Cada función de CLI debe finalizar con funciones de ret even que no devuelven nada.

Hay pocas posibilidades, el objeto de cierre de alguna manera está causando que las cosas sean más lentas, lo que podría ser cierto (pero poco probable), el jitter no alteró el método y como usted estaba disparando en una sucesión de giro rápido no tuvo que jit ese camino, invocando un camino más lento. El compilador C # en vs también puede estar emitiendo diferentes convenciones de llamada, y MethodAttributes que pueden actuar como pistas para la fluctuación de fase para realizar diferentes optimizaciones.

En última instancia, ni siquiera remotamente me preocuparía esta diferencia. Si realmente está invocando su función 3 mil millones de veces en el transcurso de su aplicación, y la diferencia en que se incurre es de 5 segundos completos, es probable que esté bien.