Linq to objects:内部查询性能

在回答其中一个问题时,我看到了两个LINQ代码的例子应该完全相同。 但我对性能感到疑惑,并发现一个代码比另一个代码快得多。 我无法理解为什么。

我从问题中获取了数据结构

public struct Strc { public decimal A; public decimal B; // more stuff } public class CLASS { public List listStrc = new List(); // other stuff } 

然后我写了简单的基准测试(使用了benchmarkdotnet库)

UPD我包括了所有要求的测试

 public class TestCases { private Dictionary dict; public TestCases() { var m = 100; var n = 100; dict = Enumerable.Range(0, m) .Select(x => new CLASS() { listStrc = Enumerable.Range(0, n) .Select(y => new Strc() { A = y % 4, B = y }).ToList() }) .ToDictionary(x => Guid.NewGuid().ToString(), x => x); } 

超过3次测试

  [Benchmark] public void TestJon_Gt3() { var result = dict.Values .SelectMany(x => x.listStrc) .Where(ls => ls.A > 3) .Select(ls => ls.B).ToArray(); } [Benchmark] public void TestTym_Gt3() { var result = dict.Values .SelectMany(x => x.listStrc.Where(l => lA > 3)) .Select(x => xB).ToArray(); } [Benchmark] public void TestDasblinkenlight_Gt3() { var result = dict.Values .SelectMany(x => x.listStrc.Select(v => v)) .Where(l => lA > 3) .Select(ls => ls.B).ToArray(); } [Benchmark] public void TestIvan_Gt3() { var result = dict.Values .SelectMany(x => x.listStrc.Where(l => lA > 3).Select(l => lB)) .ToArray(); } 

返回真实的测试

  [Benchmark] public void TestJon_True() { var result = dict.Values .SelectMany(x => x.listStrc) .Where(ls => true) .Select(ls => ls.B).ToArray(); } [Benchmark] public void TestTym_True() { var result = dict.Values .SelectMany(x => x.listStrc.Where(l => true)) .Select(x => xB).ToArray(); } [Benchmark] public void TestDasblinkenlight_True() { var result = dict.Values .SelectMany(x => x.listStrc.Select(v => v)) .Where(ls => true) .Select(ls => ls.B).ToArray(); } [Benchmark] public void TestIvan_True() { var result = dict.Values .SelectMany(x => x.listStrc.Where(l => true).Select(l => lB)) .ToArray(); } } 

我跑了那些测试

 static void Main(string[] args) { var summary = BenchmarkRunner.Run(); } 

并得到了结果

 // * Summary * BenchmarkDotNet=v0.10.9, OS=Windows 7 SP1 (6.1.7601) Processor=Intel Core i7-4770 CPU 3.40GHz (Haswell), ProcessorCount=8 Frequency=3312841 Hz, Resolution=301.8557 ns, Timer=TSC [Host] : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.6.1076.0 DefaultJob : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.6.1076.0 Method | Mean | Error | StdDev | ------------------------- |-----------:|-----------:|-----------:| TestJon_Gt3 | 655.1 us | 1.3408 us | 1.2542 us | TestTym_Gt3 | 353.1 us | 12.9535 us | 10.8167 us | TestDasblinkenlight_Gt3 | 943.9 us | 1.9563 us | 1.7342 us | TestIvan_Gt3 | 352.6 us | 0.7216 us | 0.6397 us | TestJon_True | 801.8 us | 2.7194 us | 2.2708 us | TestTym_True | 1,055.8 us | 3.0912 us | 2.7403 us | TestDasblinkenlight_True | 1,090.6 us | 2.3084 us | 2.1593 us | TestIvan_True | 677.7 us | 3.0427 us | 2.8461 us | // * Hints * Outliers TestCases.TestTym_Gt3: Default -> 2 outliers were removed TestCases.TestDasblinkenlight_Gt3: Default -> 1 outlier was removed TestCases.TestIvan_Gt3: Default -> 1 outlier was removed TestCases.TestJon_True: Default -> 2 outliers were removed TestCases.TestTym_True: Default -> 1 outlier was removed // * Legends * Mean : Arithmetic mean of all measurements Error : Half of 99.9% confidence interval StdDev : Standard deviation of all measurements 1 us : 1 Microsecond (0.000001 sec) 

我试图改变初始数据(n和m参数),但结果是稳定的,TestTym每次都比TestJon快。 TestIvan在所有测试中都是最快的。 我只想了解,为什么它更快? 或者也许我在测试期间做错了?

由于最终两个表达式都过滤掉了所有项目,因此时间差异是由于中间迭代器在组合的语句链中返回值的次数不同。

要了解正在发生的事情,请考虑从参考源实现SelectMany ,并删除参数检查:

 public static IEnumerable SelectMany(this IEnumerable source, Func> selector) { return SelectManyIterator(source, selector); } static IEnumerable SelectManyIterator(IEnumerable source, Func> selector) { foreach (TSource element in source) { foreach (TResult subElement in selector(element)) { yield return subElement; } } } 

Select是基于枚举的集合类型使用一系列不同的迭代器实现的 – WhereSelectArrayIteratorWhereSelectListIteratorWhereSelectEnumerableIterator

您的测试代码会生成A s在0到3范围内的情况,包括:

 Select(y => new Strc() { A = y % 4, B = y }) // ^^^^^^^^^ 

因此,条件Where(ls => ls.A > 3)产生匹配。

TestJon示例中, SelectMany yield return被命中10,000次,因为在过滤之前选择了所有内容。 之后, Select使用WhereSelectEnumerableIterator ,它找不到匹配项。 因此,迭代器在两个阶段中返回值的次数为10,000 + 0 = 10,000。

另一方面, TestTym在第一个状态期间过滤掉所有内容。 SelectMany获取IEnumerable的空IEnumerable s,因此迭代器在两个阶段中的任何一个阶段返回值的总次数是0 + 0 = 0。

我在查询中将条件更改为Where(l => true) ,而Tym现在比Jon慢。 为什么?

现在,两个阶段返回的项目总数相同,10,000 + 10,000 = 20,000。 现在区别在于SelectMany的嵌套循环的运行方式:

 foreach (TResult subElement in selector(element)) { yield return subElement; //^^^^^^^^^^^^^^^^^ } 

Jon的case中, selector(element)返回List 。 它似乎是foreach计算出来的,并以比Tym的情况更少的开销进行迭代,后者构造并返回新的迭代器对象。

Jon添加Select(v => v)消除了应用此优化的可能性,因此第二次更新中的结果在误差范围内。