wake-up-neo.net

Leistung von Find () vs. FirstOrDefault ()

Ähnliche Frage:
Find () vs. Where (). FirstOrDefault ()

Es wurde ein interessantes Ergebnis für die Suche nach Diana innerhalb einer großen Sequenz eines einfachen Referenztyps mit einer einzelnen Zeichenfolge-Eigenschaft erzielt.

using System;
using System.Collections.Generic;
using System.Linq;

public class Customer{
    public string Name {get;set;}
}

Stopwatch watch = new Stopwatch();        
    const string diana = "Diana";

    while (Console.ReadKey().Key != ConsoleKey.Escape)
    {
        //Armour with 1000k++ customers. Wow, should be a product with a great success! :)
        var customers = (from i in Enumerable.Range(0, 1000000)
                         select new Customer
                         {
                            Name = Guid.NewGuid().ToString()
                         }).ToList();

        customers.Insert(999000, new Customer { Name = diana }); // Putting Diana at the end :)

        //1. System.Linq.Enumerable.DefaultOrFirst()
        watch.Restart();
        customers.FirstOrDefault(c => c.Name == diana);
        watch.Stop();
        Console.WriteLine("Diana was found in {0} ms with System.Linq.Enumerable.FirstOrDefault().", watch.ElapsedMilliseconds);

        //2. System.Collections.Generic.List<T>.Find()
        watch.Restart();
        customers.Find(c => c.Name == diana);
        watch.Stop();
        Console.WriteLine("Diana was found in {0} ms with System.Collections.Generic.List<T>.Find().", watch.ElapsedMilliseconds);
    }

enter image description here

Liegt das an keinem Enumerator-Overhead in List.Find () oder diesem und vielleicht etwas anderem?

Find() läuft fast doppelt so schnell, in der Hoffnung, dass . Net das Team es in Zukunft nicht mehr als veraltet einstuft.

96

Ich konnte Ihre Ergebnisse imitieren, also habe ich Ihr Programm dekompiliert und es gibt einen Unterschied zwischen Find und FirstOrDefault.

Zunächst ist hier das dekompilierte Programm. Ich habe Ihr Datenobjekt nur zur Kompilierung zu einem anonymen Datenelement gemacht

    List<\u003C\u003Ef__AnonymousType0<string>> source = Enumerable.ToList(Enumerable.Select(Enumerable.Range(0, 1000000), i =>
    {
      var local_0 = new
      {
        Name = Guid.NewGuid().ToString()
      };
      return local_0;
    }));
    source.Insert(999000, new
    {
      Name = diana
    });
    stopwatch.Restart();
    Enumerable.FirstOrDefault(source, c => c.Name == diana);
    stopwatch.Stop();
    Console.WriteLine("Diana was found in {0} ms with System.Linq.Enumerable.FirstOrDefault().", (object) stopwatch.ElapsedMilliseconds);
    stopwatch.Restart();
    source.Find(c => c.Name == diana);
    stopwatch.Stop();
    Console.WriteLine("Diana was found in {0} ms with System.Collections.Generic.List<T>.Find().", (object) stopwatch.ElapsedMilliseconds);

Das Wichtigste dabei ist, dass FirstOrDefault für Enumerable aufgerufen wird, während Find als Methode in der Quellliste aufgerufen wird.

Also, was macht find? Dies ist die dekompilierte Find Methode

private T[] _items;

[__DynamicallyInvokable]
public T Find(Predicate<T> match)
{
  if (match == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  for (int index = 0; index < this._size; ++index)
  {
    if (match(this._items[index]))
      return this._items[index];
  }
  return default (T);
}

Es ist also sinnvoll, über ein Array von Elementen zu iterieren, da eine Liste ein Wrapper auf einem Array ist.

Allerdings verwendet FirstOrDefault in der Klasse Enumerableforeach, um die Elemente zu durchlaufen. Dies verwendet einen Iterator zur Liste und bewegt sich als nächstes. Ich denke, was Sie sehen, ist der Overhead des Iterators

[__DynamicallyInvokable]
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 source1 in source)
  {
    if (predicate(source1))
      return source1;
  }
  return default (TSource);
}

Foreach verwendet nur syntatischer Zucker das aufzählbare Muster. Schau dir dieses Bild an

enter image description here.

Ich habe auf foreach geklickt, um zu sehen, was es tut, und Sie können sehen, dass dotpeek mich zu den sinnvollen Implementierungen enumerator/current/next führen möchte.

Ansonsten sind sie im Prinzip gleich (Testen des übergebenen Prädikats, um festzustellen, ob ein Element Ihren Wünschen entspricht).

97
devshorts

Ich wette, dass FirstOrDefault über die IEnumerable -Implementierung ausgeführt wird, das heißt, dass für die Überprüfung eine Standardschleife foreach verwendet wird. List<T>.Find() ist nicht Teil von Linq ( http://msdn.Microsoft.com/en-us/library/x0b5b5bc.aspx ) und verwendet wahrscheinlich einen Standard for Schleife von 0 nach Count (oder einem anderen schnellen internen Mechanismus, der wahrscheinlich direkt auf seinem internen/umschlossenen Array arbeitet). Indem Sie den Aufwand für das Durchzählen (und das Ausführen der Versionsprüfungen, um sicherzustellen, dass die Liste nicht geändert wurde) beseitigen, ist die Find -Methode schneller.

Wenn Sie einen dritten Test hinzufügen:

//3. System.Collections.Generic.List<T> foreach
Func<Customer, bool> dianaCheck = c => c.Name == diana;
watch.Restart();
foreach(var c in customers)
{
    if (dianaCheck(c))
        break;
}
watch.Stop();
Console.WriteLine("Diana was found in {0} ms with System.Collections.Generic.List<T> foreach.", watch.ElapsedMilliseconds);

Das läuft ungefähr so ​​schnell wie das erste (25ms vs 27ms für FirstOrDefault)

BEARBEITEN: Wenn ich eine Array-Schleife hinzufüge, kommt sie der Geschwindigkeit von Find() ziemlich nahe, und wenn ich @devshorts einen Blick auf den Quellcode geworfen habe, denke ich, das ist es:

//4. System.Collections.Generic.List<T> for loop
var customersArray = customers.ToArray();
watch.Restart();
int customersCount = customersArray.Length;
for (int i = 0; i < customersCount; i++)
{
    if (dianaCheck(customers[i]))
        break;
}
watch.Stop();
Console.WriteLine("Diana was found in {0} ms with an array for loop.", watch.ElapsedMilliseconds);

Dies ist nur 5,5% langsamer als die Methode Find().

Unterm Strich: Das Durchlaufen von Array-Elementen ist schneller als der Iterationsaufwand von foreach. (Aber beide haben ihre Vor- und Nachteile. Wählen Sie einfach, was für Ihren Code logisch sinnvoll ist. Außerdem wird der kleine Geschwindigkeitsunterschied nur selten jemals zur Folge haben ein Problem, also verwenden Sie einfach, was für die Wartbarkeit/Lesbarkeit Sinn macht)

24
Chris Sinclair