LINQ方法的运行时复杂度(Big-O)有什么保证?


120

我最近已经开始使用LINQ了很多,而且我还没有真正看到任何有关LINQ方法的运行时复杂性的信息。显然,这里有许多因素在起作用,因此让我们将讨论范围限制在普通的IEnumerableLINQ-to-Objects提供程序上。此外,让我们假设任何Func以选择器/增幅器/等形式传入的都是廉价的O(1)操作。

它似乎很明显,所有的单次操作(SelectWhereCountTake/SkipAny/All,等)将是O(n)的,因为他们只需要步行的顺序一次; 尽管即使这样也会受到懒惰的影响。

对于更复杂的操作,事情变得更加模糊。集合类运算符(UnionDistinctExcept等)使用工作GetHashCode在默认情况下(据我所知),所以它似乎是合理的假设他们使用一个哈希表内,使这些操作为O(n)为好,一般。那使用的版本IEqualityComparer呢?

OrderBy需要排序,因此最有可能我们正在查看O(n log n)。如果已经排序怎么办?如果我说怎么样OrderBy().ThenBy()并为两者提供相同的密钥怎么办?

我可以使用排序或哈希查看GroupBy(和Join)。哪有

Contains在a上为O(n)List,但在a上为O(1)HashSet -LINQ是否检查基础容器以查看其是否可以加快速度?

真正的问题-到目前为止,我一直坚信作业是高效的。但是,我可以依靠吗?例如,STL容器清楚地指定了每个操作的复杂性。.NET库规范中是否对LINQ性能有任何类似的保证?

更多问题(针对评论):并
没有真正考虑开销,但是我没想到简单的Linq-to-Objects会有很多。CodingHorror帖子谈论的是Linq-to-SQL,在这里我可以理解解析查询并使SQL增加成本-对象提供程序是否也有类似成本?如果是这样,使用声明性或函数语法有什么不同?


尽管我不能真正回答您的问题,但我想评论一下,与核心功能相比,性能的大部分将是“开销”。当您拥有非常大的数据集(> 10k项)时,当然不是这种情况,因此您想知道哪种情况并不奇怪。
亨利

2
回复:“如果您使用声明性或函数式语法,会有所不同吗?” -编译器将声明性语法转换为功能性语法,因此它们将相同。
约翰·拉施(John Rasch)2010年

“ STL容器明确指定了每个操作的复杂性”。NET容器还明确指定了每个操作的复杂性。Linq扩展类似于STL算法,而不是STL容器。就像将STL算法应用于STL容器一样,您需要将Linq扩展的复杂度与.NET容器操作的复杂度相结合,以正确地分析由此产生的复杂度。正如Aaronaught的答案所提到的,这包括考虑模板专业化。
廷博

一个潜在的问题是,为什么Microsoft不更担心IList <T>优化的效用有限,因为如果开发人员的代码依赖于它的高性能,则开发人员将不得不依赖未记录的行为。
爱德华·布雷

结果集列表上的AsParallel(); 应该给你〜O(1)<O(n)
等待时间

Answers:


121

有非常少的保证,但是有一些优化:

  • 使用索引访问扩展方法,比如ElementAtSkipLast或者LastOrDefault,将检查底层式工具与否IList<T>,让你得到O(N)的O(1)访问来代替。

  • Count方法检查ICollection实现,以便此操作为O(1)而不是O(N)。

  • DistinctGroupBy Join,我相信也设置汇总的方法(UnionIntersectExcept)使用散列,所以他们应该是接近O(N),而不是O(N²)。

  • Contains检查ICollection实现,因此如果基础集合也是O(1),例如a ,则可能为O(1)HashSet<T>,但这取决于实际的数据结构,因此不能保证。哈希集会覆盖该Contains方法,这就是它们为O(1)的原因。

  • OrderBy 方法使用稳定的快速排序,因此它们是O(N log N)个平均情况。

我认为这涵盖了大多数(如果不是全部)内置扩展方法。实际上,几乎没有性能保证。Linq本身将尝试利用高效的数据结构,但是编写潜在的低效率代码并不是免费的。


怎么样IEqualityComparer过载?
tzaman 2010年

@tzaman:那他们呢?除非您使用一个效率很低的自定义IEqualityComparer,否则我无法推理出它会影响渐近复杂性。
亚伦诺特2010年

1
啊对。我没有意识到EqualityComparer工具GetHashCode以及Equals; 但这当然是很合理的。
tzaman 2010年

2
@imgen:循环连接为O(N * M),对于不相关的集合,泛化为O(N²)。Linq使用O(N + M)的哈希联接,将其推广为O(N)。假设有一个不错的哈希函数,但这在.NET中很难搞乱。
2014年

1
Orderby().ThenBy()仍然N logN还是(N logN) ^2或类似的东西?
M.kazem Akhgary

10

我早就知道,如果枚举是,则.Count()返回。.CountIList

但是我总是有点疲惫有关Set操作的运行时间复杂度:.Intersect().Except().Union()

这是.Intersect()(comments mine)的反编译BCL(.NET 4.0 / 4.5)实现:

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;
  }
}

结论:

  • 性能为O(M + N)
  • 当集合已经是集合时,实现将无法利用。(它不一定很简单,因为使用的还需要匹配。)IEqualityComparer<T>

为了完整起见,这里都为实现.Union().Except()

剧透警报:它们也具有O(N + M) 复杂度。

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

您真正可以依靠的是,Enumerable方法在一般情况下编写得很好,不会使用幼稚的算法。可能有第三方资料(博客等)描述了实际使用的算法,但是从STL算法的意义上说,这些不是官方的或保证的。

为了说明这一点,这是Enumerable.CountSystem.Core 的反射源代码(由ILSpy提供):

// 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;
    }
}

如您所见,要避免简单地枚举每个元素的幼稚解决方案。


遍历整个对象以获取Count()(如果它是IEnnumerable)对我来说似乎很天真……
Zonko

4
@Zonko:我不明白你的意思。我修改了答案,以表明Enumerable.Count除非没有明显的替代方案,否则不会迭代。您将如何减少它的幼稚?
Marcelo Cantos

好吧,是的,给定源,这些方法以最有效的方式实现。但是,最有效的方法有时是幼稚的算法,使用linq时应小心,因为它隐藏了调用的真正复杂性。如果您不熟悉要处理的对象的基础结构,则可以轻松地根据需要使用错误的方法。
Zonko

@MarceloCantos为什么不处理数组?对于ElementAtOrDefault方法,它与referencesource.microsoft.com/#System.Core/System/Linq/…
Freshblood 2015年

@Freshblood他们是。(数组实现ICollection。)但是,不了解ElementAtOrDefault。我猜数组也实现了ICollection <T>,但是这些天我的.Net还是很生锈。
马塞洛·坎托斯

3

我只是打破了反射器,它们在Contains调用时会检查底层类型。

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

正确的答案是“取决于”。这取决于基础IEnumerable的类型。我知道,对于某些集合(如实现ICollection或IList的集合),使用了特殊的代码路径,但是不能保证实际的实现会做任何特殊的事情。例如,我知道ElementAt()对于可索引集合具有特殊情况,与Count()类似。但是通常,您可能应该假设O(n)性能最差。

通常,我认为您不会找到想要的性能保证,尽管如果您确实遇到了linq运算符的特定性能问题,则始终可以针对特定集合重新实现它。另外,还有许多博客和可扩展性项目将Linq扩展到对象,以添加此类性能保证。请查看索引LINQ ,它可以扩展并添加到操作员集中以获得更多的性能优势。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.