web-dev-qa-db-fra.com

Performances de Find () par rapport à FirstOrDefault ()

Question similaire:
Find () vs. Where (). FirstOrDefault ()

Vous avez obtenu un résultat intéressant en recherchant Diana dans une grande séquence d'un type de référence simple possédant une propriété de chaîne unique.

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

Est-ce à cause de la surcharge de l'énumérateur dans List.Find () ou de ce fait, plus peut-être d'autre?

Find() est presque deux fois plus rapide, en espérant . Net l'équipe ne la marquera pas comme obsolète à l'avenir.

96
Arman McHitarian

J'ai pu imiter vos résultats, j'ai donc décompilé votre programme et il y a une différence entre Find et FirstOrDefault.

Tout d'abord, voici le programme décompilé. J'ai fait de votre objet de données un élément de données anonyme juste pour la compilation

    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);

La chose clé à noter ici est que FirstOrDefault est appelé sur Enumerable alors que Find est appelé comme méthode dans la liste des sources.

Alors, qu'est-ce que trouver fait? C'est la méthode décompilée Find

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);
}

Il est donc logique de parcourir un tableau d'éléments, car une liste est un wrapper sur un tableau.

Cependant, FirstOrDefault, dans la classe Enumerable, utilise foreach pour itérer les éléments. Cela utilise un itérateur de la liste et se déplace ensuite. Je pense que ce que vous voyez est les frais généraux de l'itérateur

[__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 est juste sucre syntatique pour utiliser le motif énumérable. Regarde cette image

enter image description here.

J'ai cliqué sur foreach pour voir ce qu'il faisait et vous pouvez voir que dotpeek veut m'emmener à la mise en œuvre énumérateur/actuelle/suivante qui a du sens.

En dehors de cela, ils sont fondamentalement les mêmes (testez le prédicat transmis pour voir si un élément est ce que vous voulez).

97
devshorts

Je parie que FirstOrDefault s'exécute via l'implémentation IEnumerable, c'est-à-dire qu'il utilisera une boucle standard foreach pour effectuer la vérification. List<T>.Find() ne fait pas partie de Linq ( http://msdn.Microsoft.com/en-us/library/x0b5b5bc.aspx ) et utilise probablement un standard for boucle de 0 À Count (ou un autre mécanisme interne rapide fonctionnant probablement directement sur son tableau interne/encapsulé). En éliminant la surcharge liée à l'énumération de (et aux vérifications de version pour vous assurer que la liste n'a pas été modifiée), la méthode Find est plus rapide.

Si vous ajoutez un troisième test:

//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);

Cela fonctionne à peu près à la même vitesse que le premier (25 ms contre 27 ms pour FirstOrDefault)

EDIT: Si j'ajoute une boucle de tableau, elle se rapproche beaucoup de la vitesse Find(), et compte tenu du code source de @devshorts, je pense que c'est bien ça:

//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);

Cela s'exécute seulement 5,5% moins vite que la méthode Find().

Donc, ligne de fond: parcourir les éléments d'un tableau est plus rapide que de traiter le temps système d'itération foreach. (mais les deux ont leurs avantages/inconvénients, alors choisissez simplement ce qui est logique pour votre code. De plus, la petite différence de vitesse ne causera que très rarement la cause un problème, donc utilisez simplement ce qui est logique pour la maintenabilité/lisibilité)

24
Chris Sinclair