Quelles garanties y a-t-il sur la complexité à court terme (Big-O) des méthodes LINQ?

j'ai récemment commencé à utiliser LINQ un peu, et je n'ai pas vraiment vu aucune mention de la complexité du temps d'exécution pour aucune des méthodes LINQ. Évidemment, il y a beaucoup de facteurs en jeu ici, alors limitons la discussion au fournisseur de LINQ-to-Objects IEnumerable . De plus, supposons que n'importe quel Func soit passé comme un sélecteur / mutateur / etc. est un bon O(1).

il semble évident que toutes les opérations mono-pass ( Select , Where , Count , Take/Skip , Any/All , etc.) seront O (n), puisqu'ils n'ont besoin de marcher la séquence qu'une seule fois; bien que même cela soit sujet à la paresse.

les choses sont plus troubles pour les opérations plus complexes; les opérateurs de type set-like( Union , Distinct , Except , etc.) fonctionnent en utilisant GetHashCode par défaut (afaik), il semble donc raisonnable de supposer qu'ils utilisent une table de hachage en interne, ce qui rend ces opérations O(n) ainsi, en général. Qu'en est-il des versions qui utilisent un IEqualityComparer ?

OrderBy aurait besoin d'une sorte, donc très probablement nous regardons O(N log n). Que faire si il est déjà trié? Que diriez-vous si je dis OrderBy().ThenBy() et fournir la même clé pour les deux?

j'ai pu voir GroupBy (et Join ) à l'aide de tri, ou le hachage. Qui est-il?

Contains serait O (n) sur un List , mais O (1) sur un HashSet - does LINQ vérifier le conteneur sous-jacent pour voir s'il peut accélérer les choses?

et la vraie question - jusqu'à présent, j'ai pris sur la foi que les opérations sont performantes. Cependant, puis-je banque sur qui? Les conteneurs STL, par exemple, précisent clairement la complexité de chaque opération. Existe-t-il des garanties similaires sur la performance de LINQ dans la spécification de bibliothèque .NET?

plus de question (en réponse aux commentaires):

Je n'y avais pas vraiment pensé, mais je ne m'attendais pas à ce qu'il y ait beaucoup de choses pour un simple Linq-to-Objects. Le post de CodingHorror parle de Linq-to - SQL, où je peux comprendre l'analyse de la requête et faire SQL ajouterait coût-y a-t-il un coût similaire pour le fournisseur D'objets aussi? Si oui, est-ce différent si vous utilisez la syntaxe déclarative ou fonctionnelle?

101
demandé sur tzaman 2010-05-10 02:29:05

5 réponses

Il y a très, très peu de garanties, mais il y a quelques optimisations:

  • méthodes D'Extension qui utilisent l'accès indexé, tels que ElementAt , Skip , Last ou LastOrDefault , vérifiera pour voir si le type sous-jacent implémente ou non IList<T> , de sorte que vous obtenez l'accès O(1) au lieu de O(N).

  • le Count les contrôles de méthode pour un ICollection mise en œuvre, de sorte que cette opération est O(1) au lieu de O(N).

  • Distinct , GroupBy Join , et je crois aussi que les méthodes de set-aggregation ( Union , Intersect et Except ) utilisent le hachage, donc elles devraient être proches de O(N) au lieu de O(N2).

  • Contains contrôle pour une ICollection mise en œuvre, de sorte qu'il peut être O (1) si le la collecte sous-jacente est également O(1), comme un HashSet<T> , mais cela dépend de la structure réelle des données et n'est pas garanti. Les ensembles de hachage supplantent la méthode Contains , c'est pourquoi ils sont O(1).

  • OrderBy " les méthodes utilisent un raccourci stable, donc ils sont O(N log N) CAS moyen.

je pense que cela couvre la plupart sinon la totalité des méthodes d'extension intégrées. Il y a vraiment de très peu de garanties de performance; Linq lui-même va essayer de tirer profit de structures de données efficaces, mais ce n'est pas une passe libre pour écrire du code potentiellement inefficace.

97
répondu Aaronaught 2010-05-09 23:16:36

Tout ce que vous pouvez vraiment compter sur est que les méthodes énumérables sont bien écrits pour le cas général et n'utiliseront pas des algorithmes naïfs. Il y a probablement des trucs de tiers (blogs, etc.) qui décrivent les algorithmes réellement utilisés, mais ils ne sont pas officiels ou garantis au sens où les algorithmes STL le sont.

pour illustrer, voici le code source reflété (avec la permission de ILSpy) pour Enumerable.Count du système.Central:

// 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 on peut voir, il va à un certain effort pour éviter la solution naïve de simplement énumérer chaque élément.

5
répondu Marcelo Cantos 2011-11-08 10:57:04

je sais depuis longtemps que .Count() retourne .Count si le dénombrement est un IList .

mais j'ai toujours été un peu las de la complexité des opérations de tournage: .Intersect() , .Except() , .Union() .

Voici L'implémentation de la BCL décompilée (.net 4.0/4.5) pour .Intersect() (commentaires mine):

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:

  • le performance is O (M + N)
  • la mise en œuvre n'est pas profiter quand les collections sont déjà . (Ce n'est pas nécessairement simple, car le IEqualityComparer<T> utilisé doit aussi correspondre.)

par souci d'exhaustivité, voici les implémentations pour .Union() et .Except() .

Spoiler alert: 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;
  }
}
4
répondu Cristi Diaconescu 2014-10-05 07:59:16

je viens de casser le réflecteur et ils vérifient le type sous-jacent quand 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);
}
2
répondu ChaosPandion 2010-05-09 22:46:59

La bonne réponse est "ça dépend". cela dépend de quel type est le IEnumerable sous-jacent. je sais que pour certaines collections (comme les collections qui mettent en œuvre ICollection ou IList) il y a des chemins de codes spéciaux qui sont utilisés, mais la mise en œuvre réelle n'est pas garantie de faire quoi que ce soit de spécial. par exemple, je sais que ElementAt() a un cas spécial pour les collections indexables, de la même façon que Count(). Mais en général, vous devriez probablement supposer le pire cas O(n) performance.

en général, je ne pense pas que vous allez trouver le genre de garanties de performance que vous voulez, bien que si vous rencontrez un problème de performance particulier avec un opérateur linq, vous pouvez toujours simplement le réimposer pour votre collection particulière. Il existe également de nombreux blogs et projets d'extensibilité qui étendent Linq aux objets pour ajouter ces types de garanties de performance. vérifier LINQ indexé qui s'étend et ajoute à l'opérateur ensemble pour plus avantages de performance.

2
répondu luke 2010-05-09 23:17:04