una tipo referencia que por pase pasaje parametros hacer funciones como comando argumentos c# lambda performance-testing lifting

tipo - C#Lambda rendimiento problemas/posibilidades/directrices



que es una referencia en c# (4)

¿Qué te hace pensar que la segunda versión no requiere ninguna elevación variable? Está definiendo el Func con una expresión Lambda, y eso requerirá los mismos fragmentos de trucos de compilación que requiere la primera versión.

Además, estás creando un Func que devuelve un Func , que dobla mi cerebro un poco y casi seguramente requerirá una reevaluación con cada llamada.

Sugiero que compile esto en modo de lanzamiento y luego use ILDASM para examinar el IL generado. Eso debería darte una idea de qué código se genera.

Otra prueba que debe ejecutar, que le dará más información, es hacer que el predicado invoque una función separada que utiliza una variable en el ámbito de clase. Algo como:

private DateTime dayToCompare; private bool LocalIsDayWithinRange(TItem i) { return i.IsDayWithinRange(dayToCompare); } public override IEnumerable<TItem> GetDayData(DateTime day) { dayToCompare = day; return this.items.Where(i => LocalIsDayWithinRange(i)); }

Eso te dirá si alzar la variable del day realmente te está costando algo.

Sí, esto requiere más código y no sugeriría que lo use. Como señaló en su respuesta a una respuesta anterior que sugirió algo similar, esto crea lo que equivale a un cierre utilizando variables locales. El punto es que usted o el compilador tienen que hacer algo como esto para que las cosas funcionen. Más allá de escribir la solución iterativa pura, no hay magia que pueda realizar que evite que el compilador tenga que hacer esto.

Mi punto aquí es que "crear el cierre" en mi caso es una asignación de variable simple. Si esto es significativamente más rápido que su versión con la expresión Lambda, entonces sabe que hay cierta ineficacia en el código que el compilador crea para el cierre.

No estoy seguro de dónde obtiene su información sobre tener que eliminar las variables gratuitas y el costo del cierre. ¿Me puede dar algunas referencias?

Estoy probando diferencias de rendimiento usando varias sintaxis de expresiones lambda. Si tengo un método simple:

public IEnumerable<Item> GetItems(int point) { return this.items.Where(i => i.IsApplicableFor(point)); }

entonces hay una elevación variable aquí relacionada con el parámetro de point porque es una variable libre desde la perspectiva de lambda. Si quisiera llamar a este método un millón de veces, ¿sería mejor mantenerlo tal cual o cambiarlo de alguna manera para mejorar su rendimiento?

¿Qué opciones tengo y cuáles son realmente viables? Según tengo entendido, tengo que deshacerme de las variables libres para que el compilador no tenga que crear clases de cierre e instanciarlas en cada llamada a este método. Esta instanciación usualmente toma una cantidad de tiempo significativa en comparación con las versiones sin cierre.

La cuestión es que me gustaría encontrar algún tipo de pautas de escritura de lambda que generalmente funcionen, porque parece que estoy perdiendo el tiempo cada vez que escribo una expresión lambda de gran impacto. Debo probarlo manualmente para asegurarme de que funcionará, porque no sé qué reglas seguir.

Método alternativo

& código de aplicación de consola de ejemplo

También he escrito una versión diferente del mismo método que no necesita ningún cambio de variable (al menos creo que no es así, pero ustedes que entienden esto, háganme saber si ese es el caso):

public IEnumerable<Item> GetItems(int point) { Func<int, Func<Item, bool>> buildPredicate = p => i => i.IsApplicableFor(p); return this.items.Where(buildPredicate(point)); }

Mira Gist aquí . Simplemente cree una aplicación de consola y copie todo el código en el archivo Program.cs dentro del bloque de namespace . Verá que el segundo ejemplo es mucho más lento, aunque no usa variables libres.

Un ejemplo contradictorio

La razón por la que me gustaría construir algunas pautas de uso óptimo de lambda es que he encontrado este problema antes y, para mi sorpresa, resultó que funcionaba más rápido cuando se utilizaba una expresión lambda de predicado .

Ahora explica eso entonces. Estoy completamente perdido aquí porque también podría ocurrir que no use lambdas cuando sé que tengo algún método de uso pesado en mi código. Pero me gustaría evitar esa situación y llegar al fondo de todo.

Editar

Tus sugerencias no parecen funcionar

Intenté implementar una clase de búsqueda personalizada que funciona internamente de forma similar a lo que hace el compilador con una variable gratuita lambda. Pero en lugar de tener una clase de cierre, he implementado miembros de instancias que simulan un escenario similar. Este es el código:

private int Point { get; set; } private bool IsItemValid(Item item) { return item.IsApplicableFor(this.Point); } public IEnumerable<TItem> GetItems(int point) { this.Point = point; return this.items.Where(this.IsItemValid); }

Curiosamente, esto funciona tan lento como la versión lenta. No sé por qué, pero parece que no hace nada más que el rápido. Reutiliza la misma funcionalidad porque estos miembros adicionales son parte de la misma instancia de objeto. De todas formas. ¡Ahora estoy extremadamente confundido !

He actualizado la fuente de Gist con esta última adición, para que pueda probarla usted mismo.


Cuando una expresión LINQ que utiliza la ejecución diferida se ejecuta dentro del mismo ámbito que incluye las variables gratuitas a las que hace referencia, el compilador debe detectar eso y no crear un cierre sobre la lambda, porque no es necesario.

La forma de verificar eso sería probándolo usando algo como esto:

public class Test { public static void ExecuteLambdaInScope() { // here, the lambda executes only within the scope // of the referenced variable ''add'' var items = Enumerable.Range(0, 100000).ToArray(); int add = 10; // free variable referenced from lambda Func<int,int> f = x => x + add; // measure how long this takes: var array = items.Select( f ).ToArray(); } static Func<int,int> GetExpression() { int add = 10; return x => x + add; // this needs a closure } static void ExecuteLambdaOutOfScope() { // here, the lambda executes outside the scope // of the referenced variable ''add'' Func<int,int> f = GetExpression(); var items = Enumerable.Range(0, 100000).ToArray(); // measure how long this takes: var array = items.Select( f ).ToArray(); } }


Hice un perfil de su punto de referencia para usted y determiné muchas cosas:

En primer lugar, gasta la mitad de su tiempo en la línea return this.GetDayData(day).ToList(); llamando a ToList . Si elimina eso y en su lugar itera manualmente sobre los resultados, puede medir las diferencias relativas en los métodos.

En segundo lugar, porque IterationCount = 1000000 y RangeCount = 1 , está cronometrando la inicialización de los diferentes métodos en lugar de la cantidad de tiempo que lleva ejecutarlos. Esto significa que su perfil de ejecución está dominado por la creación de los iteradores, el escape de registros de variables y delegados, más los cientos de colecciones de basura gen0 subsiguientes que resultan de la creación de toda esa basura.

En tercer lugar, el método "lento" es muy lento en x86, pero tan rápido como el método "rápido" en x64. Creo que esto se debe a cómo los diferentes JITters crean delegados. Si se descuenta la creación del delegado de los resultados, los métodos "rápido" y "lento" son idénticos en velocidad.

En cuarto lugar, si realmente invocas los iteradores una cantidad significativa de veces (en mi computadora, apuntar a x64, con RangeCount = 8 ), "slow" es en realidad más rápido que "foreach" y "fast" es más rápido que todos ellos.

En conclusión, el aspecto de "elevación" es insignificante. Las pruebas en mi computadora portátil muestran que capturar una variable como usted lo hace requiere 10ns extra cada vez que se crea la lambda ( no cada vez que se invoca), y eso incluye la sobrecarga extra de GC. Además, al crear el iterador en su método "foreach" es algo más rápido que crear las lambdas, en realidad invocar ese iterador es más lento que invocar las lambdas.

Si los pocos nanosegundos adicionales necesarios para crear delegados son demasiado para su aplicación, considere almacenarlos en caché. Si necesita parámetros para esos delegados (es decir, cierres), considere la posibilidad de crear sus propias clases de cierre para que pueda crearlas una vez y luego simplemente cambiar las propiedades cuando necesite reutilizar a sus delegados. Aquí hay un ejemplo:

public class SuperFastLinqRangeLookup<TItem> : RangeLookupBase<TItem> where TItem : RangeItem { public SuperFastLinqRangeLookup(DateTime start, DateTime end, IEnumerable<TItem> items) : base(start, end, items) { // create delegate only once predicate = i => i.IsDayWithinRange(day); } DateTime day; Func<TItem, bool> predicate; public override IEnumerable<TItem> GetDayData(DateTime day) { this.day = day; // set captured day to correct value return this.items.Where(predicate); } }


Su segundo método se ejecuta 8 veces más lento que el primero para mí. Como dice @DanBryant en los comentarios, esto tiene que ver con construir y llamar al delegado dentro del método, y no con el levantamiento variable.

Tu pregunta es confusa, ya que me parece que esperabas que la segunda muestra fuera más rápida que la primera. También lo leí porque el primero es de alguna manera inaceptablemente lento debido a la ''elevación variable''. La segunda muestra todavía tiene una variable libre ( point ) pero agrega una sobrecarga adicional. No entiendo por qué crees que elimina la variable gratuita.

Como confirma el código que ha publicado, la primera muestra anterior (utilizando un predicado en línea simple) realiza jsut un 10% más lento que un ciclo simple: desde su código:

foreach (TItem item in this.items) { if (item.IsDayWithinRange(day)) { yield return item; } }

Entonces, en resumen:

  • El bucle for es el enfoque más simple y es el "mejor caso".
  • El predicado en línea es un poco más lento, debido a una sobrecarga adicional.
  • Construir y llamar a un Func que devuelve Func dentro de cada iteración es significativamente más lento que cualquiera de los dos.

No creo que nada de esto sea sorprendente. La ''guía'' es usar un predicado en línea, si funciona mal, simplifíquelo moviéndose a un bucle recto.