Linq to Objects链接哪个子句VS &&性能命中是微不足道的?

mak*_*akc 10 .net c# performance linq-to-objects

以下问题:
我应该在LINQ查询中使用两个"where"子句或"&&"吗?
可以或者我应该在LINQ查询中将两个Where子句连接在一起吗?
linq样式,链接where子句vs和运营商
Jon Skeet:博客文章

大多数答案都表示,Linq To Objects在单个lambda表达式中链接where子句vs && 的性能可以忽略不计,因此可以根据您的编码风格决定使用哪一个.

我从查看IL程序集开始,你可以肯定地看到chaining where子句将导致Where扩展被调用2次而第二次调用的输入是第一次调用的结果.

var numbers = new List<int>() { 1, 2 ,3,4,5,6,7,8,9,10};
IEnumerable<int> query = numbers.Where(x=> x>2).Where(x => x<5);
Run Code Online (Sandbox Code Playgroud)

// IL

IL_005B:  ldloc.0     // numbers
IL_005C:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate3
IL_0061:  brtrue.s    IL_0076
IL_0063:  ldnull      
IL_0064:  ldftn       b__1
IL_006A:  newobj      System.Func<System.Int32,System.Boolean>..ctor
IL_006F:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate3
IL_0074:  br.s        IL_0076
IL_0076:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate3
IL_007B:  call        System.Linq.Enumerable.Where <-----------First Call
IL_0080:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate4
IL_0085:  brtrue.s    IL_009A
IL_0087:  ldnull      
IL_0088:  ldftn       b__2
IL_008E:  newobj      System.Func<System.Int32,System.Boolean>..ctor
IL_0093:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate4
IL_0098:  br.s        IL_009A
IL_009A:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate4
IL_009F:  call        System.Linq.Enumerable.Where <------------Second Call
IL_00A4:  stloc.1     // query
b__1:
IL_0000:  ldarg.0     
IL_0001:  ldc.i4.2    
IL_0002:  cgt         
IL_0004:  stloc.0     // CS$1$0000
IL_0005:  br.s        IL_0007
IL_0007:  ldloc.0     // CS$1$0000
IL_0008:  ret       
 b__2:
 IL_0000:  ldarg.0     
 IL_0001:  ldc.i4.5    
 IL_0002:  clt         
 IL_0004:  stloc.0     // CS$1$0000
 IL_0005:  br.s        IL_0007
 IL_0007:  ldloc.0     // CS$1$0000
 IL_0008:  ret    
Run Code Online (Sandbox Code Playgroud)

然后我在Win7 .Net 3.5和4.0上运行一个简单的基准测试

  static void Main(string[] args)
{               
    int size = 10000000;
    Console.WriteLine("chain clauses");
    RunTests(size,true);

    Console.WriteLine("use and");
    RunTests(size,false);               
}


static void RunTests(int size, bool chainClauses)
{
    for (int i = 1; i <= 10; i++)
     {
        if (chainClauses)
            RunTestChaining(i, size);
        else
            RunTestAnd(i, size);
        }
    }

    static void RunTestChaining(int depth, int size)
    {
        IEnumerable<string> input = Enumerable.Repeat("value", size);                      

        switch (depth)
        {
            case 1:
                input = input.Where(x => !x.Equals("1"));
                break;
            case 2:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2"));
                break;
            case 3:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3"));
                break;
            case 4:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4"));
                break;
            case 5:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4")).Where(x => !x.Equals("5"));
                break;
            case 6:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4")).Where(x => !x.Equals("5")).Where(x => !x.Equals("6"));
                break;
            case 7:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4")).Where(x => !x.Equals("5")).Where(x => !x.Equals("6")).Where(x => !x.Equals("7"));
                break;
            case 8:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4")).Where(x => !x.Equals("5")).Where(x => !x.Equals("6")).Where(x => !x.Equals("7")).Where(x => !x.Equals("8"));
                break;
            case 9:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4")).Where(x => !x.Equals("5")).Where(x => !x.Equals("6")).Where(x => !x.Equals("7")).Where(x => !x.Equals("8")).Where(x => !x.Equals("9"));
                break;
            case 10:
                input = input.Where(x => !x.Equals("1")).Where(x => !x.Equals("2")).Where(x => !x.Equals("3")).Where(x => !x.Equals("4")).Where(x => !x.Equals("5")).Where(x => !x.Equals("6")).Where(x => !x.Equals("7")).Where(x => !x.Equals("8")).Where(x => !x.Equals("9")).Where(x => !x.Equals("10"));
                break;
        }

        Stopwatch sw = Stopwatch.StartNew();
        var count = input.Count();
        sw.Stop();
        Console.WriteLine("Depth: {0} Count: {1} Time: {2}ms",
                          depth, count, sw.ElapsedMilliseconds);
    }

static void RunTestAnd(int depth, int size )
    {
        IEnumerable<string> input = Enumerable.Repeat("value", size);
        Func<string, bool> predicate = x => true;
        switch (depth)
        {
            case 1:
                predicate = x => !x.Equals("1");
                break;
            case 2:
                predicate = x => !x.Equals("1") && !x.Equals("2");
                break;
            case 3:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3");
                break;
            case 4:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3");
                break;
            case 5:
                 predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3")&& !x.Equals("5");
                break;
            case 6:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3")&& !x.Equals("5") && !x.Equals("6");
                break;
            case 7:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3")&& !x.Equals("5") && !x.Equals("6") && !x.Equals("7");
                break;
            case 8:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3")&& !x.Equals("5") && !x.Equals("6") && !x.Equals("7") && !x.Equals("8");
                break;
            case 9:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3")&& !x.Equals("5") && !x.Equals("6") && !x.Equals("7") && !x.Equals("8") && !x.Equals("9");
                break;
            case 10:
                predicate = x => !x.Equals("1") && !x.Equals("2") && !x.Equals("3")&&!x.Equals("3")&& !x.Equals("5") && !x.Equals("6") && !x.Equals("7") && !x.Equals("8") && !x.Equals("9") && !x.Equals("10");
                break;
        }
        input = input.Where(predicate);

        Stopwatch sw = Stopwatch.StartNew();
        var count = input.Count();
        sw.Stop();
        Console.WriteLine("Depth: {0} Count: {1} Time: {2}ms",
                          depth, count, sw.ElapsedMilliseconds);
    } 
Run Code Online (Sandbox Code Playgroud)

结果如下:

// .Net 3.5                                //.Net 4.0
chain clauses                               chain clauses
Depth: 1 Count: 10000000 Time: 181ms        Depth: 1 Count: 10000000 Time: 216ms
Depth: 2 Count: 10000000 Time: 248ms        Depth: 2 Count: 10000000 Time: 278ms
Depth: 3 Count: 10000000 Time: 315ms        Depth: 3 Count: 10000000 Time: 347ms
Depth: 4 Count: 10000000 Time: 378ms        Depth: 4 Count: 10000000 Time: 437ms
Depth: 5 Count: 10000000 Time: 443ms        Depth: 5 Count: 10000000 Time: 509ms
Depth: 6 Count: 10000000 Time: 514ms        Depth: 6 Count: 10000000 Time: 573ms
Depth: 7 Count: 10000000 Time: 579ms        Depth: 7 Count: 10000000 Time: 649ms
Depth: 8 Count: 10000000 Time: 644ms        Depth: 8 Count: 10000000 Time: 727ms
Depth: 9 Count: 10000000 Time: 978ms        Depth: 9 Count: 10000000 Time: 1278ms
Depth: 10 Count: 10000000 Time: 1546ms      Depth: 10 Count: 10000000 Time: 1075ms
use and                                     use and
Depth: 1 Count: 10000000 Time: 181ms        Depth: 1 Count: 10000000 Time: 202ms
Depth: 2 Count: 10000000 Time: 200ms        Depth: 2 Count: 10000000 Time: 234ms
Depth: 3 Count: 10000000 Time: 228ms        Depth: 3 Count: 10000000 Time: 267ms
Depth: 4 Count: 10000000 Time: 245ms        Depth: 4 Count: 10000000 Time: 303ms
Depth: 5 Count: 10000000 Time: 267ms        Depth: 5 Count: 10000000 Time: 335ms
Depth: 6 Count: 10000000 Time: 289ms        Depth: 6 Count: 10000000 Time: 364ms
Depth: 7 Count: 10000000 Time: 312ms        Depth: 7 Count: 10000000 Time: 397ms
Depth: 8 Count: 10000000 Time: 326ms        Depth: 8 Count: 10000000 Time: 432ms
Depth: 9 Count: 10000000 Time: 366ms        Depth: 9 Count: 10000000 Time: 462ms
Depth: 10 Count: 10000000 Time: 375ms       Depth: 10 Count: 10000000 Time: 493ms
Run Code Online (Sandbox Code Playgroud)

根据这些结果,性能显着下降,这表明您应该避免将Linq中的where子句链接到Objects.
或者有什么我想念的?

Joe*_*nta 4

是的,当这是一个显而易见的事情并且您有机会时,尝试避免将.Where()条款相互链接是一个很好的建议,就像在这个微基准测试中一样。

请注意,.NET 的 LINQ-to-Objects 实现足够智能,可以在您执行此操作时为您组合谓词。它并没有想象的那么糟糕,但是运行花哨的链式委托不会像运行包含所有逻辑的单个委托那样快速或优雅。

但是,假设您有一个任意的实例IEnumerable<T>,它可能是也可能不是某些.Where()方法的结果,并且您正在编写一个需要根据某些谓词对其进行过滤的方法。

根据此微基准测试的结果,您是否真的要重构整个代码库,以便“避免将 Linq 中的 where 子句链接到对象”,还是只是再添加一个并继续.Where()您的生活?

与往常一样,如果您对存在性能问题的应用程序运行实际性能测试(意味着性能超出了定义为“可接受”的范围),并且结果表明链接.Where()子句是瓶颈,那么您可能更有理由尝试重新思考正在发生的事情。

另外,出于好奇,我将 RunTestAnd 中重复的“3”子句更改为“4”子句,在 .NET 4.5.1 中的 4 核 Windows 8.1 x64 计算机上运行代码(发布模式,无调试器),然后运行将其初始化input为以下行后:

var input = Enumerable.Repeat("value", size).AsParallel();
Run Code Online (Sandbox Code Playgroud)

得到这些结果(为了美观而进行了输出处理,但我保证这些是实际数字):

chain clauses (parallel)                  chain clauses (serial)
Depth:  1 Count: 10000000 Time:  284ms    Depth:  1 Count: 10000000 Time:  185ms
Depth:  2 Count: 10000000 Time:  241ms    Depth:  2 Count: 10000000 Time:  248ms
Depth:  3 Count: 10000000 Time:  267ms    Depth:  3 Count: 10000000 Time:  308ms
Depth:  4 Count: 10000000 Time:  256ms    Depth:  4 Count: 10000000 Time:  370ms
Depth:  5 Count: 10000000 Time:  371ms    Depth:  5 Count: 10000000 Time:  432ms
Depth:  6 Count: 10000000 Time:  345ms    Depth:  6 Count: 10000000 Time:  667ms
Depth:  7 Count: 10000000 Time:  342ms    Depth:  7 Count: 10000000 Time:  569ms
Depth:  8 Count: 10000000 Time:  465ms    Depth:  8 Count: 10000000 Time:  627ms
Depth:  9 Count: 10000000 Time:  434ms    Depth:  9 Count: 10000000 Time:  862ms
Depth: 10 Count: 10000000 Time:  416ms    Depth: 10 Count: 10000000 Time: 1235ms
use and (parallel)                        use and (serial)
Depth:  1 Count: 10000000 Time:  263ms    Depth:  1 Count: 10000000 Time:  182ms
Depth:  2 Count: 10000000 Time:  265ms    Depth:  2 Count: 10000000 Time:  217ms
Depth:  3 Count: 10000000 Time:  239ms    Depth:  3 Count: 10000000 Time:  228ms
Depth:  4 Count: 10000000 Time:  255ms    Depth:  4 Count: 10000000 Time:  255ms
Depth:  5 Count: 10000000 Time:  272ms    Depth:  5 Count: 10000000 Time:  275ms
Depth:  6 Count: 10000000 Time:  255ms    Depth:  6 Count: 10000000 Time:  295ms
Depth:  7 Count: 10000000 Time:  268ms    Depth:  7 Count: 10000000 Time:  320ms
Depth:  8 Count: 10000000 Time:  268ms    Depth:  8 Count: 10000000 Time:  339ms
Depth:  9 Count: 10000000 Time:  305ms    Depth:  9 Count: 10000000 Time:  363ms
Depth: 10 Count: 10000000 Time:  267ms    Depth: 10 Count: 10000000 Time:  386ms
Run Code Online (Sandbox Code Playgroud)

这对我来说意味着,如果您确实发现 10 深的子句链.Where()是瓶颈,没有简单的直接重构,并且您的特定使用场景允许,请考虑尝试 PLINQ。