Considere la siguiente manipulación simple sobre una colección:
static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);
Ahora usemos Expresiones. El siguiente código es aproximadamente equivalente:
static void UsingLambda() {
Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambda(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda: {0}", tn - t0);
}
Pero quiero construir la expresión sobre la marcha, así que aquí hay una nueva prueba:
static void UsingCompiledExpression() {
var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);
var c3 = f.Compile();
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = c3(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}
Por supuesto que no es exactamente como el anterior, así que para ser justos, modifico ligeramente el primero:
static void UsingLambdaCombined() {
Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambdaCombined(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda combined: {0}", tn - t0);
}
Ahora vienen los resultados para MAX = 100000, VS2008, depuración ON:
Using lambda compiled: 23437500
Using lambda: 1250000
Using lambda combined: 1406250
Y con la depuración desactivada:
Using lambda compiled: 21718750
Using lambda: 937500
Using lambda combined: 1093750
Sorpresa . La expresión compilada es aproximadamente 17 veces más lenta que las otras alternativas. Ahora aquí vienen las preguntas:
- ¿Estoy comparando expresiones no equivalentes?
- ¿Existe un mecanismo para hacer que .NET "optimice" la expresión compilada?
- ¿Cómo expreso la misma llamada en cadena
l.Where(i => i % 2 == 0).Where(i => i > 5);
programáticamente?
Algunas estadísticas más. Visual Studio 2010, depuración activada, optimizaciones desactivadas:
Using lambda: 1093974
Using lambda compiled: 15315636
Using lambda combined: 781410
Depuración activada, optimizaciones activadas:
Using lambda: 781305
Using lambda compiled: 15469839
Using lambda combined: 468783
Depuración desactivada, optimizaciones activadas:
Using lambda: 625020
Using lambda compiled: 14687970
Using lambda combined: 468765
Nueva sorpresa. El cambio de VS2008 (C # 3) a VS2010 (C # 4) hace que sea UsingLambdaCombined
más rápido que el lambda nativo.
Ok, encontré una manera de mejorar el rendimiento compilado de lambda en más de un orden de magnitud. Aquí tienes un consejo; después de ejecutar el generador de perfiles, el 92% del tiempo se dedica a:
System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
Hmmmm ... ¿Por qué está creando un nuevo delegado en cada iteración? No estoy seguro, pero la solución sigue en una publicación separada.
fuente
Stopwatch
para tiempos en lugar deDateTime.Now
.Respuestas:
¿¡¿Podría ser que las lambdas internas no se estén compilando?!? Aquí tienes una prueba de concepto:
Y ahora los tiempos son:
¡Woot! No solo es rápido, es más rápido que el lambda nativo. ( Rascarse la cabeza ).
Por supuesto, el código anterior es simplemente demasiado doloroso de escribir. Hagamos algo de magia simple:
Y algunos tiempos, VS2010, optimizaciones activadas, depuración desactivada:
Ahora podría argumentar que no estoy generando toda la expresión de forma dinámica; solo las invocaciones encadenadas. Pero en el ejemplo anterior genero la expresión completa. Y los tiempos coinciden. Este es solo un atajo para escribir menos código.
Según tengo entendido, lo que está sucediendo es que el método .Compile () no propaga las compilaciones a lambdas internas y, por lo tanto, la invocación constante de
CreateDelegate
. Pero para entender realmente esto, me encantaría que un gurú de .NET comentara un poco sobre las cosas internas que están sucediendo.¿ Y por qué , oh, por qué ahora es más rápido que una lambda nativa?
fuente
Recientemente hice una pregunta casi idéntica:
Rendimiento de expresión compilada para delegar
La solución para mí fue que no debería llamar
Compile
alExpression
, sino que debería llamarloCompileToMethod
y compilarloExpression
en unstatic
método en un ensamblado dinámico.Al igual que:
Sin embargo, no es ideal. No estoy muy seguro de a qué tipos se aplica esto exactamente, pero creo que los tipos que el delegado toma como parámetros o devuelve el delegado tienen que ser
public
y no genéricos. Tiene que ser no genérico porque los tipos genéricos aparentemente acceden,System.__Canon
que es un tipo interno utilizado por .NET bajo el capó para tipos genéricos y esto viola lapublic
regla "tiene que ser un tipo".Para esos tipos, puede usar el aparentemente más lento
Compile
. Los detecto de la siguiente manera:Pero como dije, esto no es ideal y aún me gustaría saber por qué compilar un método en un ensamblado dinámico a veces es un orden de magnitud más rápido. Y lo digo a veces porque también he visto casos en los que un
Expression
compilado conCompile
es tan rápido como un método normal. Vea mi pregunta para eso.O si alguien conoce una forma de eludir la
public
restricción de "no no tipos" con el ensamblaje dinámico, también es bienvenida.fuente
Tus expresiones no son equivalentes y, por lo tanto, obtienes resultados sesgados. Escribí un banco de pruebas para probar esto. Las pruebas incluyen la llamada lambda regular, la expresión compilada equivalente, una expresión compilada equivalente hecha a mano, así como versiones compuestas. Deben ser números más precisos. Curiosamente, no veo mucha variación entre las versiones simples y compuestas. Y las expresiones compiladas son más lentas, naturalmente, pero muy poco. Necesita una entrada y un recuento de iteraciones lo suficientemente grandes para obtener buenos números. Hace una diferencia.
En cuanto a su segunda pregunta, no sé cómo podría sacar más rendimiento de esto, así que no puedo ayudarlo. Se ve tan bien como se va a poner.
Encontrarás mi respuesta a tu tercera pregunta en el
HandMadeLambdaExpression()
método. No es la expresión más fácil de construir debido a los métodos de extensión, pero es factible.Y los resultados en mi máquina:
fuente
El rendimiento de lambda compilado sobre los delegados puede ser más lento porque el código compilado en tiempo de ejecución puede no estar optimizado, sin embargo, el código que escribió manualmente y el compilado a través del compilador C # está optimizado.
En segundo lugar, múltiples expresiones lambda significan múltiples métodos anónimos, y llamar a cada uno de ellos requiere un poco más de tiempo que evaluar un método directo. Por ejemplo, llamando
y
son diferentes, y con el segundo se requiere un poco más de sobrecarga, ya que desde la perspectiva del compilador, en realidad son dos llamadas diferentes. Primero llamando a la x misma y luego dentro de esa declaración de llamada x.
Por lo tanto, su Lambda combinado ciertamente tendrá un rendimiento poco lento sobre una expresión lambda única.
Y esto es independiente de lo que se está ejecutando en el interior, porque todavía está evaluando la lógica correcta, pero está agregando pasos adicionales para que los realice el compilador.
Incluso después de que se compile el árbol de expresiones, no tendrá optimización y aún conservará su estructura poco compleja, evaluarlo y llamarlo puede tener validación adicional, verificación nula, etc., lo que podría ralentizar el rendimiento de las expresiones lambda compiladas.
fuente
UsingLambdaCombined
prueba está combinando múltiples funciones lambda y su rendimiento es muy cercano aUsingLambda
. Con respecto a las optimizaciones, estaba convencido de que eran manejadas por el motor JIT y, por lo tanto, el código generado en tiempo de ejecución (después de la compilación) también sería el objetivo de cualquier optimización JIT.