6

I know this question has been asked a lot by people and some even say

So, first(FirstOrDefault(predicate)) one is better in terms of performance1

and I get it, I also think that one more method call should be slightly slower, its just intuition that I have. Nevertheless, I decided to run some benchmarks in order to prove my right and boy little did I know.

This is what I had as results of running my benchmarks:

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

Not only Where(predicate).FirstOrDefault() was faster, but at what margin.

This is my benchmark code using 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;
    }
}

Since I was stunned from the results leaving me with no other choice but to ask you guys what am I doing wrong or am I missing something?


EDIT:

I added benchmarks for array vs list structure to the guys thinking it might be because of the List.

EDIT2: The saga continues and I think I am closer to the answer. Adding hardware counter to my benchmark yielded following interesting results:

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

For some reason, I can't still explain to myself why FirstOrDefault(predicate) method is yielding 2 to 3 times more branch mispredictions and ofc cache misses than Where(predicate).FirstOrDefault(), surely this has to play a bit part in the results I am observing previously.

Also, one curious thing, if you look at FirstOrDefaultArray and FirstOrDefaultList results and compare them you will see that list is 24% slower, but assemblies generated by these methods are identical to me: https://www.diffchecker.com/WSjAQlet (I stripped memory addresses of the instructions.)

kuskmen
  • 3,648
  • 4
  • 27
  • 54
  • 1
    Performance characteristic for them will be about the same if you're working against an `IEnumerable` instead of a `List`. Using `Where` will select an intermediary iterator based on the type of enumerable passed in, and I'm guessing `List` that has advantageous performance characteristics over `IEnumerable` in this regard. `FirstOrDefault(predicate)` is essentially just a foreach on an `IEnumerable`, so it doesn't take advantage of the fact you have a List. – Jonathon Chase Feb 19 '20 at 22:52
  • I am not saying it takes advantage when using `List` I expect in this benchmark `FirstOrDefault` to win by some really small margin coming from the extra method call it makes, but surely not what I am already getting. – kuskmen Feb 19 '20 at 23:18
  • 1
    In Linq2Object queries, `.Where(condition).FirstOrDefault()` certainly has a performance advantage over `FirstOrDefault(condition)`. I had to run my own tests and I didn't think it would be that much of a difference, but my findings support what you see. I also tried these against arrays and the operations were notably faster than with `List` but `Where` was still ~2x as fast. This was most interesting to consider. With intepreters like EntityFramework, the 2 boil down to the same SQL so there is no difference. In memory though there is a distinct difference. – Steve Py Feb 20 '20 at 04:55

2 Answers2

4

The generic Enumerable.Where function maps to different subclasses based on the type of argument. In this case, your argument is a List<int> so you get returned from Where a Enumerable.WhereListIterator<int> that takes a List<int> parameter. It then uses List<T>.GetEnumerator() to enumerator the list, which returns a List<T>.Enumerator struct, which uses an index to index into the List<> and return each member. This is very fast.

FirstOrDefault(Func<> pred) doesn't have this optimization, and uses foreach to traverse the list. While this also ultimately uses the same very fast List<T>.Enumerator, it calls its member methods via the IEnumerable<T> interface, which is considerably slower than calling the List<T>.Enumerator methods directly.

My testing shows that the result is FirstOrDefault(Func<> pred) takes about twice as long per element of the source list. If you write your own FirstOrDefault<T>(List<T> src, Func<T,bool> pred) using either GetEnumerator or foreach, it will run about twice as fast as the built-in FirstOrDefault.

NetMage
  • 26,163
  • 3
  • 34
  • 55
  • @JonathonChase The problem isn't getting the enumerator, it is calling `IEnumerator.Current` and `IEnumerator.MoveNext` instead of calling `List.Enumerator.Current` and `List.Enumerator.MoveNext`. Virtual calls through interfaces are slower than direct virtual calls. See [this answer](https://stackoverflow.com/a/7573073/2557128). – NetMage Feb 19 '20 at 23:25
  • I saw that, but then I changed to array and it happens all the same, this time with ratio 0.4 to 1 for Where().FirstOrDefault() whats the reasoning there? – kuskmen Feb 20 '20 at 00:23
  • 1
    It's pretty evident from testing that the 2 methods are doing something notably different but `List<>`'s enumerator isn't a factor. I tried with an array and not only is it faster than List, but the difference between `Where(x).FirstOrDefault()` and `FirstOrDefault(x)` is even more pronounced. – Steve Py Feb 20 '20 at 05:21
  • I assume this observation would apply to any and all predicate-based filtering operations - e.g. `Where(predicate).Any()` should be expected to perform faster than `Any(predicate)`. – Ian Kemp Feb 20 '20 at 08:02
  • Optimizations for "special known types" (a.k.a. sort of a specialization) like List and array cannot be used as general judgement of the performance of a method. – Ivan Stoev Feb 20 '20 at 08:07
  • @IvanStoev Agreed completely, but most of the time you will be operating on collections that are lists or arrays, so the special case ends up being the general case - hence why the LINQ team added these optimisations. – Ian Kemp Feb 20 '20 at 09:02
  • @IvanStoev - Looking at the execution and source the predicate on FirstOrDefault ended up executing a foreach where the WhereEnumerators were while loops, so it would depend entirely on the implementation of the method and what it does /w the predicate against the set. Normally this would be an issue for data reading which I use EF and had tested that they generate the same SQL and left it at that. It was pretty interesting to see a difference with linq2object. – Steve Py Feb 20 '20 at 11:41
  • @StevePy LINQ to Queryable conceptually is totally different from LINQ to Objects. That's why the general `linq` tag is useless. The discussion here is for LINQ to Objects implementation of the aforementioned methods. But even comparing the L2O implementations is incorrect, because it is implementation detail - I mean, if some methods are optimized for specific containers and some not. AFAIK the initial implementation hadn't such optimizations, then some were added for some methods, then some other are added only in net core implementations, and more might be added in the future... – Ivan Stoev Feb 20 '20 at 12:16
  • @StevePy ... The essential is that these + many other methods in L2O have O(N) linear complexity, thus should not be used in inner loops or in performance critical code. Specialized memory data structures for fast searching like dictionaries, hash sets, lookups etc. should be used instead. – Ivan Stoev Feb 20 '20 at 12:20
0

This one really got me interested, so I did a bit more digging, and the answer looks to be related to that when FirstOrDefault is called on a Where results, the predicate executes under a WhereArrayIterator or a WhereEnumerableIterator depending on the source type.

When FirstOrDefault(predicate) is used, it is iterating directly against the source array.

The difference?

FirstOrDefault(predicate)

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
        if (source == null) throw Error.ArgumentNull("source");
        if (predicate == null) throw Error.ArgumentNull("predicate");
        foreach (TSource element in source) {
            if (predicate(element)) return element;
        }
        return default(TSource);
    }

vs. WhereArrayIterator's MoveNext

public override bool MoveNext() {
            if (state == 1) {
                while (index < source.Length) {
                    TSource item = source[index];
                    index++;
                    if (predicate(item)) {
                        current = item;
                        return true;
                    }
                }
                Dispose();
            }
            return false;
        }

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source) {
        if (source == null) throw Error.ArgumentNull("source");
        IList<TSource> list = source as IList<TSource>;
        if (list != null) {
            if (list.Count > 0) return list[0];
        }
        else {
            using (IEnumerator<TSource> e = source.GetEnumerator()) {
                if (e.MoveNext()) return e.Current;
            }
        }
        return default(TSource);
    }

The key difference is that the WhereIterator classes both use while loops to iterate through the enumerable/array where the FirstOrDefault(predicate) is using foreach. Over larger sets, foreach is notably slower than while or for loops, so I suspect this will explain a good share of the variance.

Source above pulled from source reference: https://referencesource.microsoft.com/#system.core/System/Linq/Enumerable.cs,709a06a6b65427d6 and https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,8087366974af11d2

Regarding the original assumption that Where(predicate).FirstOrDefault() might be assumed to be slower than FirstOrDefault(predicate) due to the extra method call, this can be disproved by the following test:

    [Test]
    public void Test()
    {
        var data = new[] { 0, 0, 7, 0 };
        var test1 = data.FirstOrDefault(isSeven);
        var test2 = data.Where(isSeven).FirstOrDefault();
    }

    private bool isSeven(int val)
    {
        return val == 7;
    }

with a breakpoint inside the isSeven method. In both cases the predicate is only called 3 times where one might assume that it is "executed" in the Where(predicate) call which would result in 4 calls, but it is only executed when FirstOrDefault() wants to iterated through the iterator, triggering MoveNext() on the WhereEnumerator.

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • Are you implying that using foreach is twice slower than while? – kuskmen Feb 20 '20 at 11:29
  • Quite potentially. It is subjective to what the IL does in each case but it can be up to 10x slower than for/while... https://medium.com/@zsalloum/performance-of-the-loops-in-c-b2961c6d60d8 – Steve Py Feb 20 '20 at 11:37
  • I don't think so - I implemented my own `List<>.FirstOrDefault(Func<>)` extension method using `foreach` and it is faster than `Where.FirstOrDefault()`. The reason is the `FirstOrDefault` calls the enumerator methods through an interface. – NetMage Feb 20 '20 at 18:41
  • Yes, as covered in the article I linked. I didn't mean that `foreach` is specifically slower than a `while`, but the IL interactions `foreach` has with the enumerable can have performance implications. If 'foreach' is called using an `IEnumerable` reference, which the `FirstOrDefault(predicate)` does, it is 2x+ slower. I've tested this with looping over an array vs. an IEnumerable reference to an array. Execution time went from 13 seconds to 37 seconds. (50k iterations over a 100k array) I'll add that test to my example. – Steve Py Feb 20 '20 at 22:07