LINQ查询性能与紧凑查询

时间:2011-05-01 17:56:14

标签: linq quicksort performance

在检查LINQ的能力时,我编写了简单的QuickSort实现和
很高兴最终快速排序功能适合一行。
但是我注意到这个“一行”功能的性能与我原来的“直接”版本有很大不同。

这是在循环中调用快速排序函数10次的代码:

var r = new Random(DateTime.Now.Millisecond);
Stopwatch watch = new Stopwatch();
for (int i = 0; i < 10; i++)
{
    watch.Reset();
    var randomA = new int[100].Select(x => r.Next(100)).ToList();

    watch.Start();                
    var sorted = QuickSort<int>(randomA);
    watch.Stop();

    Console.WriteLine("Duration: {0} ms", watch.ElapsedMilliseconds);
}

Console.ReadLine();

QuickSort的两个实现功能与输出结果:

简易版:

IEnumerable<T> QuickSort<T>(IEnumerable<T> a) where T : IComparable<T>
{
    if (a.Count() <= 1) return a;

    var pivot = a.First();
    IEnumerable<T> lesser = a.Skip(1).Where(x => x.CompareTo(pivot) < 0);
    IEnumerable<T> bigger = a.Skip(1).Where(x => x.CompareTo(pivot) >= 0);

    return QuickSort(lesser).Concat(new T[] { pivot }).Concat(QuickSort(bigger));
}

输出
持续时间:22毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:2毫秒
持续时间:2毫秒
持续时间:3毫秒
持续时间:3毫秒

One Line实施:

IEnumerable<T> QuickSort<T>(IEnumerable<T> a) where T : IComparable<T>
{
    return a.Count() <= 1 ? a :                 
        QuickSort(a.Skip(1).Where(i => i.CompareTo(a.First()) < 0)).
        Concat(new T[] { a.First() }).                          
        Concat(QuickSort(a.Skip(1).Where(i => i.CompareTo(a.First()) >= 0)));  
}

输出
持续时间:24154毫秒
持续时间:407毫秒
持续时间:2281毫秒
持续时间:2420毫秒
持续时间:919毫秒
持续时间:48777毫秒
持续时间:4615毫秒
持续时间:3115毫秒
持续时间:1311毫秒
持续时间:1631毫秒

为什么性能会有这么大的差异?

2 个答案:

答案 0 :(得分:5)

原因是你每次都在重新计算a.First() - 如果你再次考虑这个因素:

public static IEnumerable<T> QuickSort<T>(IEnumerable<T> a) 
    where T : IComparable<T>
{
    if (a.Count() <= 1) return a;
    var pivot = a.First();
    return QuickSort(a.Skip(1).Where(i => i.CompareTo(pivot) < 0))
        .Concat(new T[] { pivot })
        .Concat(QuickSort(a.Skip(1).Where(i => i.CompareTo(pivot) >= 0)));
}

然后表现相同。


为了澄清一个实验 - 使用一个拦截器(一个平凡的,只是为了这个实验,不要在家里使用它,这是错误的)First()的实现我们可以看到实际上多少次{{ 1}}被称为。

First()

这表明在“一个班轮”中public static class TestHelper { public static int firstCalledCounter = 0; public static TSource First<TSource>(this IEnumerable<TSource> source) { firstCalledCounter++; Console.WriteLine("First called " + firstCalledCounter); IList<TSource> list = source as IList<TSource>; if (list != null) { if (list.Count > 0) { return list[0]; } else return default(TSource); } else return default(TSource); } } 被称为 236376 次,而在我们将a.First()分解出去的版本中,它被称为 98 < / strong>次。这解释了性能差异。

答案 1 :(得分:2)

这两个例子的主要罪犯都是这样的(注意这不回答问题,因为改变这个操作会对这两个例子产生影响):

if (a.Count() <= 1) return a;

在这里,您只是检查是否有一个项目,但是,您正在呼叫Count。如果IEnumerable<T>实现确实实现了ICollection<T>,它将每次都通过列表进行枚举。

由于您以递归方式调用此方法,因此在you don't really have to时最终会遍历序列中的所有项目。

相反,你应该这样做:

if (!a.Skip(1).Any()) return a;

这样,您最多只迭代两个项目(跳过第一个项目,检查是否存在第二个项目),以确定序列中是否至多有一个项目。

请注意,对于较大的N值,您将遇到与Count相同的问题;这是一个优化,因为N的值太小了。