¿LINQ requiere significativamente más ciclos de procesamiento y memoria que las técnicas de iteración de datos de nivel inferior?

8

Antecedentes

Recientemente estoy en el proceso de soportar entrevistas tecnológicas extenuantes para puestos que usan la pila .NET, algunas de las cuales incluyen preguntas tontas como esta , y algunas preguntas que son más válidas. Recientemente me encontré con un problema que puede ser válido, pero quiero consultar con la comunidad aquí para estar seguro.

Cuando un entrevistador me preguntó cómo contaría la frecuencia de las palabras en un documento de texto y clasificaría los resultados, respondí que lo haría

  1. Utilice un objeto de secuencia para colocar el archivo de texto en la memoria como una cadena.
  2. Divida la cadena en una matriz en espacios mientras ignora la puntuación.
  3. Use LINQ contra la matriz .GroupBy()y .Count()luego OrderBy()dicho recuento.

Recibí esta respuesta incorrecta por dos razones:

  1. Transmitir un archivo de texto completo a la memoria podría ser desastroso. ¿Y si fuera una enciclopedia completa? En cambio, debería transmitir un bloque a la vez y comenzar a construir una tabla hash.
  2. LINQ es demasiado costoso y requiere demasiados ciclos de procesamiento. Debería haber creado una tabla hash y, para cada iteración, solo agregar una palabra a la tabla hash si no existiera y luego aumentar su recuento.

La primera razón parece, bueno, razonable. Pero el segundo me da más pausa. Pensé que uno de los puntos de venta de LINQ es que simplemente abstrae las operaciones de nivel inferior como las tablas hash, pero que, bajo el velo, sigue siendo la misma implementación.

Pregunta

Además de unos pocos ciclos de procesamiento adicionales para llamar a métodos abstraídos, ¿LINQ requiere significativamente más ciclos de procesamiento para realizar una tarea de iteración de datos determinada que una tarea de nivel inferior (como construir una tabla hash)?

Matt Cashatt
fuente
2
Pregúntale qué idiota puso una enciclopedia completa en un solo archivo de texto.
JeffO
44
Es una de esas cosas que deben medirse. Cree 2 o 3 implementaciones y registre el rendimiento. Las generalizaciones sobre LINQ o la técnica X no son útiles. Yo diría que es deficiente para el entrevistador declarar que usa LINQ una respuesta "incorrecta". Aunque en el servidor de carga pesada, el procesamiento de cada milisegundo cuenta.
Lord Tydus
1
Un rápido google para "pruebas de rendimiento linq para objetos y bucles" encontró bastantes resultados. Algunos incluyen el código fuente que puede usar para probarlo usted mismo. Mira esto , esto y esto .
Oded
1
En cuanto a las entrevistas, recuerde que hay algunos programadores de C ++ de la "vieja escuela" que piensan que debe reinventar la rueda en lugar de usar las bibliotecas .NET. También se encontrará con VB'ers de la vieja escuela que desean hacer todo el código de acceso a datos a mano en lugar de usar LINQ y EF.
jfrankcarr
1
Oded, esos ejemplos en los enlaces que proporcionó están muy equivocados. No puedo entrar en todos los detalles en un comentario, pero tome el segundo enlace. Compara "foreach x if x = toFind stop" con una consulta linq que hace el equivalente de "select * from list donde x gusta toFind" La diferencia es que la primera se detiene cuando encuentra la primera instancia, la consulta linq siempre se repite cada entrada y devolverá una colección de TODOS los elementos que coincidan con el patrón de búsqueda. Muy diferente. Eso no es porque LINQ está roto, es porque utilizó la consulta incorrecta.
Ian

Respuestas:

9

Diría que la principal debilidad de esta respuesta es menos su uso de Linq y más operadores específicos elegidos. GroupBytoma cada elemento y lo proyecta a una clave y un valor que entran en una búsqueda. En otras palabras, cada palabra agregará algo a la búsqueda.

La implementación ingenua .GroupBy(e => e)almacenará una copia de cada palabra en el material fuente, haciendo que la búsqueda final sea casi tan grande como el material fuente original. Incluso si proyectamos el valor con .GroupBy(e => e, e => null), estamos creando una gran búsqueda de valores pequeños.

Lo que queremos es un operador que conserve solo la información necesaria, que es una copia de cada palabra y el recuento de esa palabra hasta el momento. Para eso, podemos usar Aggregate:

words.Aggregate(new Dictionary<string, int>(), (counts, word) => 
{
    int currentCount;
    counts.TryGetValue(word, currentCount);
    counts[word] = currentCount + 1;
    return counts;
} 

A partir de aquí, hay varias formas en que podríamos intentar hacer esto más rápido:

  1. En lugar de crear muchas cadenas mientras se divide, podríamos pasar estructuras que hagan referencia a la cadena original y al segmento que contiene la palabra, y solo copiar el segmento cuando resulte ser una clave única
  2. Use Parallel Linq para agregar varios núcleos y luego combine los resultados . Esto es trivial en comparación con el trabajo de piernas requerido para hacer esto a mano.
Chris Pitman
fuente
Todos los buenos puntos Chris, gracias. Voy a abstenerme de aceptar un poco, ya que la pregunta es más general y esencialmente respondida por Oded en los comentarios anteriores. Solo quiero darle la oportunidad de darle la respuesta primero. Gracias de nuevo por su comprensión, sin embargo, que es genial.
Matt Cashatt
6

Creo que tuvo un escape estrecho, el entrevistador realmente no sabía de qué estaba hablando. Peor aún, él cree que hay una respuesta "correcta". Si él fuera alguien para quien quisieras trabajar, esperaría que él tomara tu respuesta inicial, descubriera por qué la elegiste y luego te desafíe a mejorarla si puede encontrar problemas con ella.

LINQ asusta a la gente porque parece magia. En realidad es muy simple (tan simple que necesitarías ser un genio para idearlo)

var result = from item in collection where item=>item.Property > 3 select item;

Se compila en:

IEnumerable<itemType> result = collection.Where(item=>item.property >3);

(Por favor, no grite si tengo la sintaxis incorrecta, es después de la medianoche y estoy en la cama :))

Donde es un método de extensión en IEnumerable que toma una lambda. La lambda es simplemente (en este caso) compilada a un delegado:

bool AMethod(ItemType item)
{
    return item.property >3;
}

El método Where simplemente agrega TODAS las instancias del elemento donde AMethod devuelve verdadero a una colección que se devuelve.

No hay razón para que sea más lento que hacer un foreach y agregar todos los elementos coincidentes a una colección en ese ciclo. De hecho, el método de extensión Where probablemente esté haciendo exactamente eso. La verdadera magia viene cuando necesitas inyectar una alternativa donde los criterios.

Como mencioné anteriormente en mi comentario, algunos de los ejemplos vinculados son muy incorrectos. Y es este tipo de información errónea la que causa los problemas.

Finalmente, si la entrevista te hubiera dado una oportunidad, podrías haber dicho eso:

  • LINQ es fácil de leer, especialmente cuando comienzas a presentar proyecciones y agrupaciones interesantes. El código fácil de leer es fácil [y | ier] para mantener el código, lo cual es una victoria.

  • Sería realmente fácil medir el rendimiento si realmente fuera un cuello de botella y reemplazarlo por otra cosa.

Ian
fuente
En general, estoy de acuerdo con usted, pero el comportamiento del método Where: el método Where no agrega todos los elementos coincidentes a una colección. Almacena la información necesaria para filtrar elementos en el árbol de expresión. Si el iterador devuelto no se usa realmente, no ocurrirá ningún filtrado.
Codismo
Excelente punto, debería haber mencionado eso. En su ejemplo, utilizan el iterador devuelto. Esta fue la locura de su prueba. Para extraer el único valor encontrado (todos los elementos en sus datos de prueba eran únicos) tenían un foreach que iteraba el enumerable resultante para mostrar el resultado. Por supuesto, solo hubo un resultado, por lo que solo imprimió la única respuesta. Madness :)
Ian
Si bien no he usado LINQ, una cosa que me parece desagradable es que optimiza cosas como Countalgunos escenarios estrechos que funcionan mal con la encapsulación. Concatene una lista de millones de elementos y un iterador de cuatro elementos, y Countdebería requerir aproximadamente 5 operaciones, pero en su lugar requerirá un millón. Desearía que MS lo definiera IEnhancedEnumeratorcon un int Move(int)método que devolvería 0 en caso de éxito, o devolvería la cantidad de déficit en caso de falla (al hacerlo Move(1000003)en una lista creada recientemente a List<T>.Enumeratorpartir de una lista de un millón de elementos devolvería 3). Cualquier implementación ...
supercat
... de IEnumerable<T>podría incluirse en una implementación de IEnhancedEnumerator, pero los tipos que implementan IEnhancedEnumeratordirectamente podrían permitir órdenes de magnitud de aceleración en muchas operaciones, e incluso cosas como el retorno de Appendpodrían exponer la capacidad de búsqueda rápida de los componentes.
Supercat