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
- Utilice un objeto de secuencia para colocar el archivo de texto en la memoria como una cadena.
- Divida la cadena en una matriz en espacios mientras ignora la puntuación.
- Use LINQ contra la matriz
.GroupBy()
y.Count()
luegoOrderBy()
dicho recuento.
Recibí esta respuesta incorrecta por dos razones:
- 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.
- 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)?
fuente
Respuestas:
Diría que la principal debilidad de esta respuesta es menos su uso de Linq y más operadores específicos elegidos.
GroupBy
toma 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
:A partir de aquí, hay varias formas en que podríamos intentar hacer esto más rápido:
fuente
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)
Se compila en:
(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:
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.
fuente
Count
algunos escenarios estrechos que funcionan mal con la encapsulación. Concatene una lista de millones de elementos y un iterador de cuatro elementos, yCount
debería requerir aproximadamente 5 operaciones, pero en su lugar requerirá un millón. Desearía que MS lo definieraIEnhancedEnumerator
con unint 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 hacerloMove(1000003)
en una lista creada recientemente aList<T>.Enumerator
partir de una lista de un millón de elementos devolvería 3). Cualquier implementación ...IEnumerable<T>
podría incluirse en una implementación deIEnhancedEnumerator
, pero los tipos que implementanIEnhancedEnumerator
directamente podrían permitir órdenes de magnitud de aceleración en muchas operaciones, e incluso cosas como el retorno deAppend
podrían exponer la capacidad de búsqueda rápida de los componentes.