LINQ Lambda与查询语法的性能比较

22

今天我在项目中看到了一个LINQ查询语法,它可以像这样从List中计算符合特定条件的项数:

int temp = (from A in pTasks 
            where A.StatusID == (int)BusinessRule.TaskStatus.Pending     
            select A).ToList().Count();

我考虑重构它,使用 Count(Func) 进行重写,以使其更易读。我认为这样也会提高性能,所以我写了下面的代码:

int UnassignedCount = pTasks.Count(x => x.StatusID == (int)BusinessRule.TaskStatus.Pending);

但是当我使用StopWatch检查时,Lambda表达式所用的时间总是比查询语法更长:

Stopwatch s = new Stopwatch();
s.Start();
int UnassignedCount = pTasks.Count(x => x.StatusID == (int)BusinessRule.TaskStatus.Pending);
s.Stop();
Stopwatch s2 = new Stopwatch();
s2.Start();
int temp = (from A in pTasks 
            where A.StatusID == (int)BusinessRule.TaskStatus.Pending
            select A).ToList().Count();
s2.Stop();

有人可以解释一下为什么吗?


8
你是否改变了这些查询的执行顺序?结果是否仍然相同? - Farhad Jabiyev
3
在这之前,你是否进行了即时编译的预热? - Yuval Itzchakov
1
和@FarhadJabiyev一样的问题。pTasks下面是什么源?这是SQL数据库,还是仅仅是Linq-to-objects? - Jeppe Stig Nielsen
1
这是针对对象的 LINQ。 - Ehsan Sajjad
1
通过改变顺序,@FarhadJabiyev也可以得到相同的结果。 - Ehsan Sajjad
显示剩余8条评论
2个回答

26
我已经模拟了你的情况,并且这些查询的执行时间确实存在差异。但是,造成这种差异的原因并不是查询的语法。无论您使用方法还是查询语法,都会产生相同的结果,因为查询表达式在编译之前会被转换为其lambda表达式。
但是,如果您注意到这两个查询根本不一样。在编译之前,您的第二个查询将被翻译为其lambda语法(您可以从查询中删除ToList(),因为它是多余的)。
pTasks.Where(x => x.StatusID == (int)BusinessRule.TaskStatus.Pending).Count();

现在我们有两个使用Lambda语法的Linq查询。一个是我之前提到的,另一个是这个:

pTasks.Count(x => x.StatusID == (int)BusinessRule.TaskStatus.Pending);

现在问题是:
为什么这两个查询的执行时间会有差异?

让我们来找答案:
我们可以通过回顾以下内容理解这种差异:
- .Where(this IEnumerable<TSource> source, Func<TSource, bool> predicate).Count(this IEnumerable<TSource> source)

- Count(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

这里是 Count(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 的实现:

public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    int count = 0;
    foreach (TSource element in source) {
        checked {
            if (predicate(element)) count++;
        }
    }
    return count;
}

这里是Where(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null) 
        throw Error.ArgumentNull("source");
    if (predicate == null) 
        throw Error.ArgumentNull("predicate");
    if (source is Iterator<TSource>) 
        return ((Iterator<TSource>)source).Where(predicate);
    if (source is TSource[]) 
        return new WhereArrayIterator<TSource>((TSource[])source, predicate);
    if (source is List<TSource>) 
        return new WhereListIterator<TSource>((List<TSource>)source, predicate);
    return new WhereEnumerableIterator<TSource>(source, predicate);
}

请注意Where()的实现。如果您的集合是List,它将返回WhereListIterator(),但Count()只会迭代源。我认为他们在WhereListIterator实现方面进行了一些加速。之后,我们调用Count()方法,该方法不需要谓词作为输入,只会迭代过滤后的集合。

关于加速实现WhereListIterator的问题:

我在SO上发现了this问题:LINQ性能Count vs Where and Count。你可以在那里阅读@Matthew Watson的答案。他解释了这两个查询之间的性能差异。结果是: Where迭代器避免了间接虚拟表调用,而直接调用迭代器方法。 正如你在那个答案中看到的,将会发出call指令而不是callvirt。而callvirtcall慢:

来自书籍CLR via C#

使用callvirt IL指令调用虚拟实例方法时,CLR会发现用于调用的对象的实际类型,然后通过多态方式调用该方法。为了确定类型,用于调用的变量不能为null。换句话说,在编译此调用时,JIT编译器生成的代码会验证变量的值是否为null。如果为null,则callvirt指令会导致CLR抛出NullReferenceException异常。这个额外的检查意味着callvirt IL指令的执行速度比call指令略慢。

2
我认为这是一个很好的调查,但我认为你应该将int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)int Count<TSource>(this IEnumerable<TSource> source)进行比较。 - Enigmativity
@Enigmativity 是的,在调用 Count 后,它只会迭代筛选后的集合。我已经更新了我的回答。 - Farhad Jabiyev

5
像Farhad所说,Where(x).Count()Count(x)的实现不同。第一个实例化了一个额外的迭代器,在我的电脑上花费大约30,000个滴答(与集合大小无关)。
另外,ToList并非免费。它会分配内存。这需要时间。在我的电脑上,它大致会将执行时间翻倍。(因此与集合大小成线性关系)
此外,调试需要启动时间。因此很难一次性准确地测量性能。我建议像这个例子那样使用循环。然后忽略第一组结果。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var pTasks = Task.GetTasks();
            for (int i = 0; i < 5; i++)
            {

                var s1 = Stopwatch.StartNew();
                var count1 = pTasks.Count(x => x.StatusID == (int) BusinessRule.TaskStatus.Pending);
                s1.Stop();
                Console.WriteLine(s1.ElapsedTicks);

                var s2 = Stopwatch.StartNew();
                var count2 =
                    (
                        from A in pTasks
                        where A.StatusID == (int) BusinessRule.TaskStatus.Pending
                        select A
                        ).ToList().Count();
                s2.Stop();
                Console.WriteLine(s2.ElapsedTicks);

                var s3 = Stopwatch.StartNew();
                var count3 = pTasks.Where(x => x.StatusID == (int) BusinessRule.TaskStatus.Pending).Count();
                s3.Stop();
                Console.WriteLine(s3.ElapsedTicks);


                var s4 = Stopwatch.StartNew();
                var count4 =
                    (
                        from A in pTasks
                        where A.StatusID == (int) BusinessRule.TaskStatus.Pending
                        select A
                        ).Count();
                s4.Stop();
                Console.WriteLine(s4.ElapsedTicks);

                var s5 = Stopwatch.StartNew();
                var count5 = pTasks.Count(x => x.StatusID == (int) BusinessRule.TaskStatus.Pending);
                s5.Stop();
                Console.WriteLine(s5.ElapsedTicks);
                Console.WriteLine();
            }
            Console.ReadLine();
        }
    }

    public class Task
    {
        public static IEnumerable<Task> GetTasks()
        {
            for (int i = 0; i < 10000000; i++)
            {
                yield return new Task { StatusID = i % 3 };
            }
        }

        public int StatusID { get; set; }
    }

    public class BusinessRule
    {
        public enum TaskStatus
        {
            Pending,
            Other
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接