J'ai récemment commencé à utiliser LINQ un peu, et je n'ai vraiment vu aucune mention de la complexité d'exécution pour aucune des méthodes LINQ. De toute évidence, il existe de nombreux facteurs en jeu ici, alors limitons la discussion au fournisseur simple IEnumerable
LINQ-to-Objects. De plus, supposons que tout Func
passé comme sélecteur/mutateur/etc. est une opération O(1)) bon marché.
Il semble évident que toutes les opérations en un seul passage (Select
, Where
, Count
, Take/Skip
, Any/All
, Etc.) être O (n), car ils n'ont besoin de parcourir la séquence qu'une seule fois; même si cela est sujet à la paresse.
Les choses sont plus troubles pour les opérations plus complexes; les opérateurs de type ensemble (Union
, Distinct
, Except
, etc.) fonctionnent en utilisant GetHashCode
par défaut (afaik), il semble donc raisonnable de supposer ils utilisent une table de hachage en interne, ce qui rend ces opérations O(n) également, en général. Qu'en est-il des versions qui utilisent un IEqualityComparer
?
OrderBy
aurait besoin d'un tri, donc très probablement nous regardons O (n log n). Et si c'est déjà trié? Et si je dis OrderBy().ThenBy()
et que je fournis la même clé aux deux?
Je pouvais voir GroupBy
(et Join
) en utilisant le tri ou le hachage. Lequel est-ce?
Contains
serait O(n) sur un List
, mais O(1) sur un HashSet
- LINQ vérifie-t-il le conteneur sous-jacent pour voir s'il peut accélérer les choses?
Et la vraie question - jusqu'à présent, je croyais que les opérations étaient performantes. Cependant, puis-je miser sur cela? Les conteneurs STL, par exemple, spécifient clairement la complexité de chaque opération. Existe-t-il des garanties similaires sur les performances de LINQ dans la spécification de la bibliothèque .NET?
Plus de question (en réponse aux commentaires):
Je n'avais pas vraiment pensé aux frais généraux, mais je ne m'attendais pas à ce qu'il y en ait beaucoup pour de simples Linq-to-Objects. La publication CodingHorror parle de Linq-to-SQL, où je peux comprendre que l'analyse de la requête et que SQL augmenterait le coût - y a-t-il un coût similaire pour le fournisseur d'objets également? Si oui, est-ce différent si vous utilisez la syntaxe déclarative ou fonctionnelle?
Il y a très, très peu de garanties, mais il y a quelques optimisations:
Les méthodes d'extension qui utilisent un accès indexé, telles que ElementAt
, Skip
, Last
ou LastOrDefault
, vérifieront si le type sous-jacent implémente IList<T>
, pour que vous obteniez O(1) accès au lieu de O (N).
La méthode Count
recherche une implémentation ICollection
, de sorte que cette opération est O(1) au lieu de O (N).
Distinct
, GroupBy
Join
, et je crois aussi les méthodes d'agrégation des ensembles (Union
, Intersect
et Except
) utilisent le hachage, ils doivent donc être proches de O(N) au lieu de O (N²).
Contains
recherche une implémentation ICollection
, donc peut être O(1) si la collection sous-jacente est également O (1 ), tel qu'un HashSet<T>
, mais cela dépend de la structure réelle des données et n'est pas garanti. Les jeux de hachage remplacent la méthode Contains
, c'est pourquoi ils sont O (1).
Les méthodes OrderBy
utilisent un tri rapide stable, elles sont donc O (N log N) cas moyen.
Je pense que cela couvre la plupart sinon toutes les méthodes d'extension intégrées. Il y a vraiment très peu de garanties de performance; Linq lui-même tentera de tirer parti de structures de données efficaces, mais ce n'est pas une passe gratuite pour écrire du code potentiellement inefficace.
Tout ce sur quoi vous pouvez vraiment compter, c'est que les méthodes Enumerable sont bien écrites pour le cas général et n'utiliseront pas d'algorithmes naïfs. Il existe probablement des éléments tiers (blogs, etc.) qui décrivent les algorithmes réellement utilisés, mais ils ne sont ni officiels ni garantis dans le sens où les algorithmes STL le sont.
Pour illustrer, voici le code source réfléchi (gracieuseté d'ILSpy) pour Enumerable.Count
de System.Core:
// System.Linq.Enumerable
public static int Count<TSource>(this IEnumerable<TSource> source)
{
checked
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
ICollection<TSource> collection = source as ICollection<TSource>;
if (collection != null)
{
return collection.Count;
}
ICollection collection2 = source as ICollection;
if (collection2 != null)
{
return collection2.Count;
}
int num = 0;
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
num++;
}
}
return num;
}
}
Comme vous pouvez le voir, cela fait un effort pour éviter la solution naïve de simplement énumérer chaque élément.
Je sais depuis longtemps que .Count()
renvoie .Count
Si l'énumération est un IList
.
Mais j'étais toujours un peu las de la complexité d'exécution des opérations Set: .Intersect()
, .Except()
, .Union()
.
Voici l'implémentation BCL (.NET 4.0/4.5) décompilée pour .Intersect()
(commente le mien):
private static IEnumerable<TSource> IntersectIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
Set<TSource> set = new Set<TSource>(comparer);
foreach (TSource source in second) // O(M)
set.Add(source); // O(1)
foreach (TSource source in first) // O(N)
{
if (set.Remove(source)) // O(1)
yield return source;
}
}
Conclusions:
IEqualityComparer<T>
Utilisé doit également correspondre.)Pour être complet, voici les implémentations de .Union()
et .Except()
.
Alerte spoiler: eux aussi ont O (N + M) complexité.
private static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
Set<TSource> set = new Set<TSource>(comparer);
foreach (TSource source in first)
{
if (set.Add(source))
yield return source;
}
foreach (TSource source in second)
{
if (set.Add(source))
yield return source;
}
}
private static IEnumerable<TSource> ExceptIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
Set<TSource> set = new Set<TSource>(comparer);
foreach (TSource source in second)
set.Add(source);
foreach (TSource source in first)
{
if (set.Add(source))
yield return source;
}
}
Je viens d'éclater le réflecteur et ils vérifient le type sous-jacent lorsque Contains
est appelé.
public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
ICollection<TSource> is2 = source as ICollection<TSource>;
if (is2 != null)
{
return is2.Contains(value);
}
return source.Contains<TSource>(value, null);
}
La bonne réponse est "ça dépend". cela dépend du type du IEnumerable sous-jacent. Je sais que pour certaines collections (comme les collections qui implémentent ICollection ou IList), il existe des chemins de code spéciaux qui sont utilisés, mais la mise en œuvre réelle n'est pas garantie de faire quelque chose de spécial. par exemple, je sais que ElementAt () a un cas particulier pour les collections indexables, de même avec Count (). Mais en général, vous devriez probablement supposer le pire des cas O(n) performance.
En général, je ne pense pas que vous trouverez le type de garanties de performances que vous souhaitez, bien que si vous rencontrez un problème de performances particulier avec un opérateur linq, vous pouvez toujours le réimplémenter pour votre collection particulière. Il existe également de nombreux blogs et projets d'extensibilité qui étendent Linq aux objets pour ajouter ce type de garanties de performances. consultez LINQ indexé qui étend et ajoute à l'ensemble d'opérateurs pour plus d'avantages en termes de performances.