Where(predicate).FirstOrDefault() vs FirstOrDefault(predicate) 的基准测试令人惊讶还是错误?

kus*_*men 6 .net c# linq performance benchmarking

我知道这个问题已经被 了很多 ,甚至有人说

所以, first(FirstOrDefault(predicate)) 一个在性能方面更好1

我明白了,我也认为再调用一个方法应该稍微慢一点,这只是我的直觉。尽管如此,我还是决定运行一些基准测试来证明我的权利,而我却知之甚少。

这是我运行基准测试的结果:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7-3630QM CPU 2.40GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
  [Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
  Job-XMZTSC : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
Runtime=.NET 4.7.2  

Method                            N      Mean         Error      StdDev    Ratio    RatioSD
WherePlusFirstOrDefaultArray    10000   31.44 us    0.288 us    0.270 us    0.40    0.00
FirstOrDefaultArray             10000   78.47 us    0.679 us    0.635 us    1.00    0.00
WherePlusFirstOrDefaultList     10000   54.27 us    1.070 us    1.099 us    0.69    0.02
FirstOrDefaultList              10000   100.84 us   1.722 us    1.611 us    1.29    0.02
WherePlusFirstOrDefaultArray    100000  325.41 us   4.840 us    4.527 us    0.39    0.01
FirstOrDefaultArray             100000  829.85 us   16.513 us   15.446 us   1.00    0.00
WherePlusFirstOrDefaultList     100000  558.10 us   5.507 us    5.151 us    0.67    0.01
FirstOrDefaultList              100000  1,026.93 us 17.648 us   16.508 us   1.24    0.02
WherePlusFirstOrDefaultArray    1000000 3,255.46 us 9.615 us    7.507 us    0.40    0.01
FirstOrDefaultArray             1000000 8,134.15 us 108.425 us  101.420 us  1.00    0.00
WherePlusFirstOrDefaultList     1000000 5,477.63 us 70.584 us   66.024 us   0.67    0.01
FirstOrDefaultList              1000000 9,987.54 us 64.239 us   60.089 us   1.23    0.02
Run Code Online (Sandbox Code Playgroud)

不仅Where(predicate).FirstOrDefault()速度更快,而且速度更快。

这是我的基准代码使用 BenchmarkDotNet

[SimpleJob(RuntimeMoniker.Net472)]
public class Benchmarks
{
    private int[] _array;
    private List<int> _list;

    [Params(10000, 100000, 1000000)]
    public int N;

    [GlobalSetup]
    public void Setup()
    {
        _array = new int[N];
        _list = new List<int>(N);

        _array = Enumerable
            .Repeat(0, N - 1).ToArray();
        _list = Enumerable
            .Repeat(0, N - 1).ToList();

        _array[N - 2] = 7;
        _list[N - 2] = 7;
    }

    [Benchmark]
    public int WherePlusFirstOrDefaultArray()
    {
        var seven = _array.Where(n => n == 7).FirstOrDefault();

        return seven;
    }

    [Benchmark(Baseline = true)]
    public int FirstOrDefaultArray()
    {
        var seven = _array.FirstOrDefault(n => n == 7);

        return seven;
    }

    [Benchmark]
    public int WherePlusFirstOrDefaultList()
    {
        var seven = _list.Where(n => n == 7).FirstOrDefault();

        return seven;
    }

    [Benchmark]
    public int FirstOrDefaultList()
    {
        var seven = _list.FirstOrDefault(n => n == 7);

        return seven;
    }
}
Run Code Online (Sandbox Code Playgroud)

由于我对结果感到震惊,让我别无选择,只能问你们我做错了什么还是我错过了什么?


编辑:

我向那些认为可能是因为List.

EDIT2:传奇还在继续,我想我更接近答案了。将硬件计数器添加到我的基准测试中产生了以下有趣的结果:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
Intel Core i7-3630QM CPU 2.40GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
  [Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
  Job-ZTIMEH : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
Runtime=.NET 4.7.2

Method                             N     Mean        Error      StdDev     Ratio    RatioSD CacheMisses/Op  BranchMispredictions/Op
WherePlusFirstOrDefaultArray    1000000 3.222 ms    0.0224 ms   0.0210 ms    0.39     0.01      885                  327
FirstOrDefaultArray             1000000 8.166 ms    0.1992 ms   0.1863 ms   1.00      0.00     1,795                 810
WherePlusFirstOrDefaultList     1000000 5.564 ms    0.1066 ms   0.1228 ms   0.68      0.02     1,051                 503
FirstOrDefaultList              1000000 10.161 ms   0.1816 ms   0.1699 ms   1.24      0.03     3,451                1,442
Run Code Online (Sandbox Code Playgroud)

出于某种原因,我仍然无法向自己解释为什么FirstOrDefault(predicate)方法产生的分支错误预测和 ofc 缓存未命中是2 到 3 倍Where(predicate).FirstOrDefault(),这肯定在我之前观察到的结果中起到了一定的作用。

此外,一个奇怪的事,如果你看看FirstOrDefaultArrayFirstOrDefaultList结果,并加以比较,你会看到该列表是24%速度较慢,但这些方法生成的程序集是相同的对我说:https://www.diffchecker.com/WSjAQlet(我剥指令的内存地址。)

Net*_*age 4

泛型Enumerable.Where函数根据参数的类型映射到不同的子类。在本例中,您的参数是 a ,因此您从带有参数的aList<int>返回。然后它使用枚举器列表,返回 a ,它使用索引来索引并返回每个成员。这非常快。WhereEnumerable.WhereListIterator<int>List<int>List<T>.GetEnumerator()List<T>.Enumerator structList<>

FirstOrDefault(Func<> pred)没有这个优化,用来foreach遍历列表。虽然这最终也使用相同的非常快的List<T>.Enumerator方法,但它通过接口调用其成员方法IEnumerable<T>,这比直接调用方法要慢得多List<T>.Enumerator

我的测试表明,结果FirstOrDefault(Func<> pred)大约是源列表中每个元素的两倍时间。FirstOrDefault<T>(List<T> src, Func<T,bool> pred)如果您使用GetEnumerator或编写自己的foreach,它的运行速度大约是内置FirstOrDefault.