Recientemente comencé a usar LINQ bastante, y realmente no he visto ninguna mención de la complejidad del tiempo de ejecución para ninguno de los métodos LINQ. Obviamente, hay muchos factores en juego aquí, así que limitemos la discusión al IEnumerable
proveedor simple de LINQ-to-Objects. Además, supongamos que cualquiera que se Func
pase como selector / mutador / etc.es una operación barata de O (1).
Parece obvio que todas las operaciones de un solo paso ( Select
, Where
, Count
, Take/Skip
, Any/All
, etc.) serán O (n), ya que sólo tienen que caminar la secuencia de una vez; aunque incluso esto está sujeto a la pereza.
Las cosas son más turbias para las operaciones más complejas; la puesta a punto como operadores ( Union
, Distinct
, Except
, etc.) mediante el trabajo GetHashCode
por defecto (que yo sepa), por lo que parece razonable suponer que están utilizando una tabla hash interna, haciendo que estas operaciones O (n), así, en general. ¿Qué pasa con las versiones que usan un IEqualityComparer
?
OrderBy
necesitaría una especie, por lo que lo más probable es que estemos mirando O (n log n). ¿Y si ya está ordenado? ¿Qué tal si digo OrderBy().ThenBy()
y proporciono la misma clave para ambos?
Pude ver GroupBy
(y Join
) usar clasificación o hash. Cual es
Contains
sería O (n) en a List
, pero O (1) en a HashSet
- ¿LINQ verifica el contenedor subyacente para ver si puede acelerar las cosas?
Y la verdadera pregunta: hasta ahora, lo he estado tomando con fe en que las operaciones están funcionando. Sin embargo, ¿puedo contar con eso? Los contenedores STL, por ejemplo, especifican claramente la complejidad de cada operación. ¿Existen garantías similares sobre el rendimiento de LINQ en la especificación de la biblioteca .NET?
Más pregunta (en respuesta a los comentarios):
Realmente no había pensado en la sobrecarga, pero no esperaba que hubiera mucho para Linq-to-Objects simples. La publicación CodingHorror está hablando de Linq-to-SQL, donde puedo entender que analizar la consulta y hacer que SQL agregue un costo: ¿también hay un costo similar para el proveedor de Objetos? Si es así, ¿es diferente si usa la sintaxis declarativa o funcional?
Respuestas:
Hay muy, muy pocas garantías, pero hay algunas optimizaciones:
Los métodos de extensión que utilizan acceso indexado, tales como
ElementAt
,Skip
,Last
oLastOrDefault
, comprobará para ver si o no los implementos tipo subyacenteIList<T>
, por lo que se obtiene O (1) acceso en lugar de O (N).El
Count
método busca unaICollection
implementación, por lo que esta operación es O (1) en lugar de O (N).Distinct
,GroupBy
Join
y creo que también los métodos de agregación de conjuntos (Union
,Intersect
yExcept
) usan hash, por lo que deberían estar cerca de O (N) en lugar de O (N²).Contains
comprueba unaICollection
implementación, por lo que puede ser O (1) si la colección subyacente también es O (1), como aHashSet<T>
, pero esto depende de la estructura de datos real y no está garantizado. Los conjuntos hash anulan elContains
método, por eso son O (1).OrderBy
Los métodos usan una ordenación rápida estable, por lo que son casos promedio O (N log N).Creo que cubre la mayoría, si no todos, de los métodos de extensión integrados. Realmente hay muy pocas garantías de rendimiento; El propio Linq intentará aprovechar las estructuras de datos eficientes, pero no es un pase libre para escribir código potencialmente ineficiente.
fuente
IEqualityComparer
sobrecargas?IEqualityComparer
, no puedo razonar para que afecte la complejidad asintótica.EqualityComparer
implementosGetHashCode
tan bienEquals
; pero, por supuesto, tiene mucho sentido.Orderby().ThenBy()
quietoN logN
o es(N logN) ^2
o algo así?Hace tiempo que sé que
.Count()
regresa.Count
si la enumeración es unIList
.Pero yo era siempre un poco cansados de la complejidad en tiempo de ejecución de las operaciones Set:
.Intersect()
,.Except()
,.Union()
.Aquí está la implementación BCL (.NET 4.0 / 4.5) descompilada para
.Intersect()
(comentarios míos):Conclusiones:
IEqualityComparer<T>
también debe coincidir).Para completar, aquí están las implementaciones para
.Union()
y.Except()
.Alerta de spoiler: ellos también tienen complejidad O (N + M) .
fuente
Todo lo que realmente puede confiar es que los métodos Enumerable están bien escritos para el caso general y no usarán algoritmos ingenuos. Probablemente haya material de terceros (blogs, etc.) que describa los algoritmos realmente en uso, pero estos no son oficiales ni están garantizados en el sentido en que lo son los algoritmos STL.
Para ilustrar, aquí está el código fuente reflejado (cortesía de ILSpy)
Enumerable.Count
de System.Core:Como puede ver, se requiere un esfuerzo para evitar la ingenua solución de simplemente enumerar cada elemento.
fuente
Enumerable.Count
no se repite a menos que no haya una alternativa obvia. ¿Cómo lo habrías hecho menos ingenuo?Acabo de romper el reflector y verifican el tipo subyacente cuando
Contains
se llama.fuente
La respuesta correcta es "depende". depende del tipo de IEnumerable subyacente. Sé que para algunas colecciones (como las colecciones que implementan ICollection o IList) hay rutas de código especiales que se utilizan, sin embargo, no se garantiza que la implementación real haga nada especial. por ejemplo, sé que ElementAt () tiene un caso especial para colecciones indexables, de manera similar con Count (). Pero, en general, probablemente debería asumir el peor rendimiento de O (n).
En general, no creo que vaya a encontrar el tipo de garantías de rendimiento que desea, aunque si se encuentra con un problema de rendimiento particular con un operador de linq, siempre puede volver a implementarlo para su colección particular. También hay muchos blogs y proyectos de extensibilidad que extienden Linq a Objects para agregar este tipo de garantías de rendimiento. Eche un vistazo a Indexed LINQ, que se extiende y se suma al conjunto de operadores para obtener más beneficios de rendimiento.
fuente