Recentemente, comecei a usar bastante o LINQ e não vi nenhuma menção à complexidade do tempo de execução para nenhum dos métodos do LINQ. Obviamente, há muitos fatores em jogo aqui, então vamos restringir a discussão ao IEnumerable
provedor LINQ-to-Objects simples . Além disso, vamos supor que qualquer Func
passado como um seletor / mutador / etc. seja uma operação O (1) barata.
Parece evidente que todas as operações de uma só passagem ( Select
, Where
, Count
, Take/Skip
, Any/All
, etc.) vai ser O (n), uma vez que só precisa de caminhar a sequência de uma vez; embora mesmo isso esteja sujeito à preguiça.
As coisas são mais obscuras para as operações mais complexas; o conjunto-como operadores ( Union
, Distinct
, Except
, etc.) trabalho usando GetHashCode
por padrão (afaik), então parece razoável supor que eles estão usando um hash-table internamente, tornando estas operações O (n), bem como, em geral. E as versões que usam um IEqualityComparer
?
OrderBy
precisaria de uma classificação, então provavelmente estamos olhando para O (n log n). E se já estiver classificado? Que tal se eu disser OrderBy().ThenBy()
e fornecer a mesma chave para ambos?
Eu poderia ver GroupBy
(e Join
) usando classificação ou hash. Qual é?
Contains
seria O (n) em a List
, mas O (1) em a HashSet
- o LINQ verifica o contêiner subjacente para ver se ele pode acelerar as coisas?
E a verdadeira questão - até agora, tenho acreditado que as operações são eficazes. No entanto, posso apostar nisso? Os contêineres STL, por exemplo, especificam claramente a complexidade de cada operação. Há alguma garantia semelhante no desempenho do LINQ na especificação da biblioteca .NET?
Mais perguntas (em resposta aos comentários):
Não pensei muito sobre a sobrecarga, mas não esperava que houvesse muito para o simples Linq-to-Objects. O post CodingHorror está falando sobre Linq-to-SQL, onde posso entender que analisar a consulta e fazer SQL aumentaria os custos - há um custo semelhante para o provedor de objetos também? Em caso afirmativo, é diferente se você estiver usando a sintaxe declarativa ou funcional?
Respostas:
Existem muito, muito poucas garantias, mas existem algumas otimizações:
Os métodos de extensão que usam acesso indexado, como
ElementAt
,Skip
,Last
ouLastOrDefault
, irá verificar para ver se ou não os implementos tipo subjacenteIList<T>
, para que você obtenha O (1) o acesso em vez de O (N).O
Count
método verifica se há umaICollection
implementação, de modo que essa operação seja O (1) em vez de O (N).Distinct
,GroupBy
Join
e eu acredito que também os métodos de agregação de conjuntos (Union
,Intersect
eExcept
) usam hash, então eles devem estar próximos de O (N) em vez de O (N²).Contains
verifica por umICollection
implementação, então pode ser O (1) se a coleção subjacente também for O (1), como aHashSet<T>
, mas isso depende da estrutura de dados real e não é garantido. Conjuntos de hash substituem oContains
método, é por isso que eles são O (1).OrderBy
os métodos usam um quicksort estável, então eles são O (N log N) caso médio.Acho que cobre a maioria, senão todos os métodos de extensão integrados. Existem realmente poucas garantias de desempenho; O próprio Linq tentará tirar proveito de estruturas de dados eficientes, mas não é um passe livre para escrever código potencialmente ineficiente.
fonte
IEqualityComparer
sobrecargas?IEqualityComparer
, não posso pensar que afete a complexidade assintótica.EqualityComparer
implementosGetHashCode
tambémEquals
; mas é claro que isso faz todo o sentido.Orderby().ThenBy()
ainda éN logN
ou é(N logN) ^2
ou algo parecido?Eu sei há muito tempo que
.Count()
retorna.Count
se a enumeração for umIList
.Mas eu estava sempre um pouco cansado sobre a complexidade de tempo de execução das acções constantes:
.Intersect()
,.Except()
,.Union()
.Aqui está a implementação BCL (.NET 4.0 / 4.5) descompilada para
.Intersect()
(comentários meus):Conclusões:
IEqualityComparer<T>
também precisa corresponder.)Para completar, aqui estão as implementações para
.Union()
e.Except()
.Alerta de spoiler: eles também têm complexidade O (N + M) .
fonte
Tudo o que você pode realmente confiar é que os métodos Enumerable são bem escritos para o caso geral e não usam algoritmos ingênuos. Provavelmente, há coisas de terceiros (blogs, etc.) que descrevem os algoritmos realmente em uso, mas eles não são oficiais ou garantidos no sentido em que os algoritmos STL são.
Para ilustrar, aqui está o código-fonte refletido (cortesia de ILSpy)
Enumerable.Count
de System.Core:Como você pode ver, é difícil evitar a solução ingênua de simplesmente enumerar cada elemento.
fonte
Enumerable.Count
não itera, a menos que não haja alternativa óbvia. Como você o teria tornado menos ingênuo?Acabei de abrir o refletor e eles verificam o tipo subjacente quando
Contains
é chamado.fonte
A resposta correta é "depende". depende de que tipo é o IEnumerable subjacente. Eu sei que para algumas coleções (como coleções que implementam ICollection ou IList) existem codepaths especiais que são usados. No entanto, a implementação real não tem garantia de fazer nada de especial. por exemplo, eu sei que ElementAt () tem um caso especial para coleções indexáveis, da mesma forma com Count (). Mas, em geral, você provavelmente deve assumir o pior caso de desempenho O (n).
Em geral, eu não acho que você vai encontrar o tipo de garantia de desempenho que deseja, embora se você encontrar um problema de desempenho específico com um operador linq, você pode sempre reimplementá-lo para sua coleção específica. Além disso, existem muitos blogs e projetos de extensibilidade que estendem o Linq para objetos para adicionar esses tipos de garantias de desempenho. confira o LINQ indexado que se estende e adiciona ao conjunto do operador para obter mais benefícios de desempenho.
fonte