LINQ Lambda与查询语法性能



我今天在我的项目中看到了一个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();

有人能解释一下为什么会这样吗?

我模拟了你的情况。是的,这些查询的执行时间是不同的。但是,造成这种差异的原因并不是查询的语法。不管您是否使用了方法或查询语法。两者都会产生相同的结果,因为查询表达式在编译之前会被转换为它们的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:LINQ性能计数与位置和计数中发现了这个问题。你可以在那里阅读@Matthew Watson的答案。他解释了这两个查询之间的性能差异。结果是:Where迭代器避免了间接的虚拟表调用,而是直接调用迭代器方法正如您在该应答中看到的,将发出call指令而不是callvirt。并且,callvirtcall:慢

摘自CLR via C#:

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

正如Farhad所说,Where(x).Count()Count(x)的实现各不相同。第一个实例化了一个额外的迭代器,在我的电脑上,这个迭代器的成本约为30.000 ticks(无论集合大小)

此外,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
        }
    }
}

最新更新