我最近开始使用LINQ了,我还没有真正看到任何LINQ方法的运行时复杂性。显然,这里有很多因素在起作用,所以让我们将讨论局限于普通的IEnumerable
LINQ-to-Objects提供者。此外,假设任何作为选择器/ mutator /等传入的Func
是廉价的O(1)操作。
很明显,所有单遍操作(Select
,Where
,Count
,Take/Skip
,Any/All
等等都是O (n),因为他们只需要走一次序列;虽然这甚至会受到懒惰的影响。
对于更复杂的行动来说,事情变得更加模糊;类似于集合的运算符(Union
,Distinct
,Except
等)默认使用GetHashCode
(afaik),因此假设它们正在使用似乎是合理的内部的哈希表,通常也可以使这些操作成为O(n)。那些使用IEqualityComparer
的版本呢?
OrderBy
需要排序,所以我们很可能会看O(n log n)。如果它已经排序了怎么办?如果我说OrderBy().ThenBy()
并为两者提供相同的密钥怎么样?
我可以使用排序或散列来查看GroupBy
(和Join
)。这是什么?
Contains
将是List
上的O(n),但是HashSet
上的O(1) - LINQ会检查基础容器以查看它是否可以加快速度?
真正的问题 - 到目前为止,我一直坚信运营是高效的。但是,我可以依靠吗?例如,STL容器清楚地指定了每个操作的复杂性。 .NET库规范中的LINQ性能是否有类似的保证?
更多问题(回应评论):
没有真正想过开销,但我没想到会有很多简单的Linq-to-Objects。 CodingHorror帖子讨论的是Linq-to-SQL,在那里我可以理解解析查询并使SQL增加成本 - 对象提供者也有类似的成本吗?如果是这样,如果您使用声明性或功能性语法,它会有所不同吗?
答案 0 :(得分:104)
保证非常非常少,但有一些优化:
使用索引访问的扩展方法(例如ElementAt
,Skip
,Last
或LastOrDefault
将检查基础类型是否实现IList<T>
,以便您获得O(1)访问而不是O(N)。
Count
方法检查ICollection
实现,以便此操作为O(1)而不是O(N)。
Distinct
,GroupBy
Join
,我也相信集合汇总方法(Union
,Intersect
和Except
)使用散列,因此它们应该接近O(N)而不是O(N²)。
Contains
检查ICollection
实现,如果基础集合也是O(1),可能为O(1),例如HashSet<T>
,但这取决于实际的数据结构,并不能保证。散列集覆盖Contains
方法,这就是它们为O(1)的原因。
OrderBy
方法使用稳定的快速排序,因此它们的平均值为O(N log N)。
我认为这涵盖了大多数(如果不是全部)内置扩展方法。实际保证很少; Linq本身将尝试利用高效的数据结构,但编写可能效率低下的代码并不是免费的。
答案 1 :(得分:8)
你可以真正依赖的是,Enumerable方法是针对一般情况编写的,不会使用朴素算法。可能有第三方的东西(博客等)描述了实际使用的算法,但从STL算法的意义上来说,这些并不是官方的或保证的。
为了说明,这里是来自System.Core的Enumerable.Count
的反映源代码(由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;
}
}
正如您所看到的,它需要付出一些努力来避免简单地枚举每个元素的天真解决方案。
答案 2 :(得分:7)
我很早就知道如果枚举是.Count()
,.Count
会返回IList
。
但我总是对Set操作的运行时复杂性感到有点厌倦:.Intersect()
,.Except()
,.Union()
。
这是.Intersect()
的反编译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;
}
}
结论:
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;
}
}
答案 3 :(得分: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);
}
答案 4 :(得分:3)
正确的答案是“它取决于”。它取决于底层IEnumerable的类型。我知道对于某些集合(如实现ICollection或IList的集合),使用了特殊的代码路径,但实际的实现并不能保证做任何特殊的事情。例如,我知道ElementAt()具有可索引集合的特殊情况,类似于Count()。但总的来说,你应该假设O(n)性能最差的情况。
一般来说,我认为你不会找到你想要的那种性能保证,但是如果你遇到linq运算符的特定性能问题,你总是可以为你的特定集合重新实现它。此外,还有许多博客和可扩展性项目将Linq扩展到Objects以添加这些性能保证。检查Indexed LINQ扩展并添加到运算符集以获得更多性能优势。