Tengo una clase como esta:
public class MyClass
{
public int Value { get; set; }
public bool IsValid { get; set; }
}
De hecho, es mucho más grande, pero esto recrea el problema (rareza).
Quiero obtener la suma de Value
, donde la instancia es válida. Hasta ahora, he encontrado dos soluciones para esto.
El primero es este:
int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
La segunda, sin embargo, es esta:
int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
Quiero obtener el método más eficiente. Al principio, pensé que el segundo sería más eficiente. Entonces la parte teórica de mí comenzó a decir "Bueno, uno es O (n + m + m), el otro es O (n + n). El primero debería funcionar mejor con más inválidos, mientras que el segundo debería funcionar mejor con menos". Pensé que actuarían por igual. EDITAR: Y luego @Martin señaló que el Dónde y el Seleccionar se combinaron, por lo que en realidad debería ser O (m + n). Sin embargo, si mira a continuación, parece que esto no está relacionado.
Así que lo puse a prueba.
(Son más de 100 líneas, así que pensé que era mejor publicarlo como un Gist.)
Los resultados fueron ... interesantes.
Con 0% de tolerancia de empate:
Las escalas están a favor de Select
y Where
, en aproximadamente ~ 30 puntos.
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36
Con 2% de tolerancia de empate:
Es lo mismo, excepto que para algunos estaban dentro del 2%. Yo diría que es un margen mínimo de error. Select
y Where
ahora solo tengo una ventaja de ~ 20 puntos.
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37
Con 5% de tolerancia de empate:
Esto es lo que yo diría que es mi margen de error máximo. Lo hace un poco mejor para el Select
, pero no mucho.
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31
Con 10% de tolerancia de empate:
Esto está fuera de mi margen de error, pero todavía estoy interesado en el resultado. Porque da la Select
y Where
el punto de veinte conducir que ha tenido desde hace un tiempo.
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21
Con 25% de tolerancia de empate:
Esto está muy, muy lejos de mi margen de error, pero todavía estoy interesado en el resultado, porque todavíaSelect
y (casi) mantienen su ventaja de 20 puntos. Parece que lo está superando en unos pocos, y eso es lo que le da el liderazgo.Where
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0
Ahora, supongo que la ventaja de 20 puntos venía del medio, donde los dos están obligados a obtener alrededor la misma actuación. Podría intentar registrarlo, pero sería una gran cantidad de información para asimilar. Supongo que un gráfico sería mejor.
Entonces eso fue lo que hice.
Muestra que la Select
línea se mantiene estable (esperada) y que la Select + Where
línea sube (esperada). Sin embargo, lo que me desconcierta es por qué no cumple con los Select
50 o antes: de hecho, esperaba antes de los 50, ya que se tenía que crear un enumerador adicional para el Select
yWhere
. Quiero decir, esto muestra la ventaja de 20 puntos, pero no explica por qué. Este, supongo, es el punto principal de mi pregunta.
¿Por qué se comporta así? ¿Debo confiar en eso? Si no, ¿debería usar el otro o este?
Como @KingKong mencionó en los comentarios, también puede usar Sum
la sobrecarga de un lambda. Entonces mis dos opciones ahora se cambian a esto:
Primero:
int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
Segundo:
int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);
Lo haré un poco más corto, pero:
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0
La ventaja de veinte puntos sigue ahí, lo que significa que no tiene que ver con el Where
ySelect
combinación combinación señalada por @Marcin en los comentarios.
¡Gracias por leer mi muro de texto! Además, si está interesado, aquí está la versión modificada que registra el CSV que toma Excel.
mc.Value
.Where
+Select
no causa dos iteraciones separadas sobre la colección de entrada. LINQ to Objects lo optimiza en una iteración. Leer más en mi blogRespuestas:
Select
itera una vez sobre todo el conjunto y, para cada elemento, realiza una rama condicional (comprobación de validez) y una+
operación.Where+Select
crea un iterador que omite elementos no válidos (noyield
los hace), realizando un+
solo en los elementos válidos.Entonces, el costo de a
Select
es:Y para
Where+Select
:Dónde:
p(valid)
es la probabilidad de que un elemento de la lista sea válido.cost(check valid)
es el costo de la sucursal que verifica la validezcost(yield)
es el costo de construir el nuevo estado delwhere
iterador, que es más complejo que el simple iterador queSelect
usa la versión.Como puede ver, para una versión dada
n
, laSelect
versión es una constante, mientras que laWhere+Select
versión es una ecuación lineal conp(valid)
una variable. Los valores reales de los costos determinan el punto de intersección de las dos líneas y, dado quecost(yield)
pueden ser diferentescost(+)
, no necesariamente se intersecan enp(valid)
= 0.5.fuente
cost(append)
? Sin embargo, una respuesta realmente buena, lo mira desde un ángulo diferente en lugar de solo estadísticas.Where
no crea nada, solo devuelve un elemento a la vez de lasource
secuencia si solo llena el predicado.IEnumerable.Select(IEnumerable, Func)
yIQueryable.Select(IQueryable, Expression<Func>)
. Tienes razón en que LINQ no hace "nada" hasta que iteras sobre la colección, que es probablemente lo que querías decir.Aquí hay una explicación en profundidad de lo que está causando las diferencias de tiempo.
La
Sum()
función para seIEnumerable<int>
ve así:En C #,
foreach
es solo azúcar sintáctico para la versión de .Net de un iterador, (no debe confundirse con ) . Entonces, el código anterior se traduce realmente a esto:IEnumerator<T>
IEnumerable<T>
Recuerde, las dos líneas de código que está comparando son las siguientes
Ahora aquí está el pateador:
LINQ utiliza ejecución diferida . Por lo tanto, si bien puede parecer que
result1
itera sobre la colección dos veces, en realidad solo itera sobre ella una vez. LaWhere()
condición se aplica realmente duranteSum()
, dentro de la llamada aMoveNext()
(Esto es posible gracias a la magia deyield return
) .Esto significa que, para
result1
, el código dentro delwhile
bucle,solo se ejecuta una vez por cada elemento con
mc.IsValid == true
. En comparación,result2
ejecutará ese código para cada artículo de la colección. Es por eso queresult1
es generalmente más rápido.(Sin embargo, tenga en cuenta que llamar a la
Where()
condición dentroMoveNext()
todavía tiene algunos gastos generales pequeños, por lo que si la mayoría / todos los elementos lo tienenmc.IsValid == true
, ¡enresult2
realidad será más rápido!)Esperemos que ahora esté claro por qué
result2
suele ser más lento. Ahora me gustaría explicar por qué dije en los comentarios que estas comparaciones de rendimiento de LINQ no importan .Crear una expresión LINQ es barato. Llamar a las funciones de delegado es barato. Asignar y recorrer un iterador es barato. Pero es aún más barato no hacer estas cosas. Por lo tanto, si encuentra que una declaración LINQ es el cuello de botella en su programa, en mi experiencia, reescribirla sin LINQ siempre la hará más rápida que cualquiera de los diversos métodos LINQ.
Entonces, su flujo de trabajo LINQ debería verse así:
Afortunadamente, los cuellos de botella de LINQ son raros. Diablos, los cuellos de botella son raros. He escrito cientos de declaraciones LINQ en los últimos años, y he terminado reemplazando <1%. Y la mayoría de ellos se debieron a LINQ2EF la pobre optimización de SQL de , en lugar de ser culpa de LINQ.
Entonces, como siempre, escriba primero un código claro y sensato, y espere hasta después de que se haya perfilado para preocuparse por las micro optimizaciones.
fuente
Cosa graciosa. ¿Sabes cómo se
Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)
define? ¡Utiliza elSelect
método!Entonces, en realidad, todo debería funcionar casi igual. Hice una investigación rápida por mi cuenta, y aquí están los resultados:
Para las siguientes implementaciones:
mod
significa: cada 1 de losmod
elementos no es válido: paramod == 1
cada elemento no es válido, paramod == 2
los elementos impares no son válidos, etc. La colección contiene10000000
elementos.Y resultados para la colección con
100000000
artículos:Como se puede ver,
Select
ySum
los resultados son bastante consistente a través de todos losmod
valores. Sin embargowhere
ywhere
+select
no lo son.fuente
Supongo que la versión con Where filtra los ceros y no son un tema para Sum (es decir, no está ejecutando la adición). Por supuesto, esto es una suposición, ya que no puedo explicar cómo ejecutar una expresión lambda adicional y llamar a varios métodos supera a una simple adición de un 0.
Un amigo mío sugirió que el hecho de que el 0 en la suma pueda causar una severa penalización de rendimiento debido a las comprobaciones de desbordamiento. Sería interesante ver cómo funcionaría esto en un contexto no verificado.
fuente
unchecked
hacen un poquito, un poquito mejor para elSelect
.Al ejecutar la siguiente muestra, me queda claro que el único momento en que + Select puede superar a Select es, de hecho, cuando descarta una buena cantidad (aproximadamente la mitad en mis pruebas informales) de los posibles elementos de la lista. En el pequeño ejemplo a continuación, obtengo aproximadamente los mismos números de ambas muestras cuando el Where omite aproximadamente 4mil artículos de 10mil. Corrí en el lanzamiento y reordené la ejecución de where + select vs select con los mismos resultados.
fuente
Select
?Si necesita velocidad, simplemente hacer un bucle directo es probablemente su mejor opción. Y hacerlo
for
tiende a ser mejor queforeach
(suponiendo que su colección sea de acceso aleatorio, por supuesto).Aquí están los tiempos que obtuve con un 10% de elementos que no son válidos:
Y con 90% de elementos inválidos:
Y aquí está mi código de referencia ...
Por cierto, estoy de acuerdo con la suposición de Stilgar : las velocidades relativas de sus dos casos varían según el porcentaje de elementos no válidos, simplemente porque la cantidad de trabajo que
Sum
debe hacer varía en el caso "Dónde".fuente
En lugar de tratar de explicar a través de la descripción, voy a adoptar un enfoque más matemático.
Dado el siguiente código que debe aproximarse a lo que LINQ está haciendo internamente, los costos relativos son los siguientes:
Seleccione solo:
Nd + Na
Donde + Seleccionar:
Nd + Md + Ma
Para descubrir el punto donde se cruzarán, necesitamos hacer un poco de álgebra:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)
Lo que esto significa es que para que el punto de inflexión sea del 50%, el costo de una invocación de delegado debe ser aproximadamente el mismo que el costo de una adición. Como sabemos que el punto de inflexión real era de aproximadamente el 60%, podemos trabajar hacia atrás y determinar que el costo de una invocación de delegado para @ It'sNotALie era en realidad aproximadamente 2/3 del costo de una adición, lo cual es sorprendente, pero eso es lo que dicen sus números.
fuente
Creo que es interesante que el resultado de MarcinJuraszek sea diferente del de It'sNotALie. En particular, los resultados de MarcinJuraszek comienzan con las cuatro implementaciones en el mismo lugar, mientras que los resultados de It'sNotALie se cruzan por la mitad. Explicaré cómo funciona esto desde la fuente.
Supongamos que hay
n
elementos totales, ym
elementos válidos.La
Sum
función es bastante simple. Simplemente recorre el enumerador: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)Por simplicidad, supongamos que la colección es una lista. Tanto Select como WhereSelect crearán a
WhereSelectListIterator
. Esto significa que los iteradores reales generados son los mismos. En ambos casos, hay unSum
que recorre un iterador, elWhereSelectListIterator
. La parte más interesante del iterador es el método MoveNext .Como los iteradores son iguales, los bucles son iguales. La única diferencia está en el cuerpo de los bucles.
El cuerpo de estas lambdas tiene un costo muy similar. La cláusula where devuelve un valor de campo, y el predicado ternario también devuelve un valor de campo. La cláusula select devuelve un valor de campo, y las dos ramas del operador ternario devuelven un valor de campo o una constante. La cláusula select combinada tiene la rama como operador ternario, pero WhereSelect usa la rama en
MoveNext
.Sin embargo, todas estas operaciones son bastante baratas. La operación más cara hasta ahora es la sucursal, donde nos costará una predicción incorrecta.
Otra operación costosa aquí es la
Invoke
. Invocar una función lleva bastante más tiempo que agregar un valor, como ha demostrado Branko Dimitrijevic.También se pesa la acumulación comprobada en
Sum
. Si el procesador no tiene un indicador de desbordamiento aritmético, entonces también podría ser costoso verificarlo.Por lo tanto, los costos interesantes son: es:
n
+m
) * Invocar +m
*checked+=
n
* Invocar +n
*checked+=
Por lo tanto, si el costo de Invoke es mucho más alto que el costo de la acumulación controlada, entonces el caso 2 siempre es mejor. Si son casi iguales, entonces veremos un equilibrio cuando aproximadamente la mitad de los elementos sean válidos.
Parece que en el sistema de MarcinJuraszek, marcado + = tiene un costo insignificante, pero en los sistemas de It'sNotALie y Branko Dimitrijevic, marcado + = tiene costos significativos. Parece que es el más caro en el sistema It'sNotALie ya que el punto de equilibrio es mucho mayor. No parece que nadie haya publicado resultados de un sistema donde la acumulación cuesta mucho más que la invocación.
fuente