web-dev-qa-db-fra.com

Quelles sont les garanties sur la complexité d'exécution (Big-O) des méthodes LINQ?

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?

108
tzaman

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, GroupByJoin, 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.

107
Aaronaught

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.

8
Marcelo Cantos

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:

  • la performance est O (M + N)
  • l'implémentation ne profite pas lorsque les collections sont déjà des ensembles . (Cela peut ne pas être nécessairement simple, car le 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;
  }
}
8

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);
}
3
ChaosPandion

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.

3
luke