Comparaison C # et OrderBy

105

Je peux trier une liste en utilisant Trier ou Trier par. Lequel est le plus rapide? Les deux travaillent-ils sur le même algorithme?

List<Person> persons = new List<Person>();
persons.Add(new Person("P005", "Janson"));
persons.Add(new Person("P002", "Aravind"));
persons.Add(new Person("P007", "Kazhal"));

1.

persons.Sort((p1,p2)=>string.Compare(p1.Name,p2.Name,true));

2.

var query = persons.OrderBy(n => n.Name, new NameComparer());

class NameComparer : IComparer<string>
{
    public int Compare(string x,string y)
    {
      return  string.Compare(x, y, true);
    }
}
user215675
la source
22
Je ne peux pas croire qu'aucune des réponses ne mentionne cela, mais la plus grande différence est la suivante: OrderBy fait une copie triée du tableau ou de la liste, tandis que Sort le trie en fait.
PRMan
2
comme titre de comparaison, je voudrais ajouter que OrderBy est stable et que le tri est stable jusqu'à 16 éléments car jusqu'à 16 éléments le tri par insertion est utilisé si les éléments sont plus que cela, puis il passe à d'autres algos instables Edit: stable signifie maintenir l'ordre relatif d'éléments ayant la même clé.
Eklavyaa
@PRMan Non, OrderBy crée un énumérable paresseux. Ce n'est que si vous appelez une méthode telle que ToList sur l'énumérateur renvoyé que vous obtenez une copie triée.
Stewart
1
@Stewart, Vous ne considérez pas Array.Copy ou Collection.Copy into TElement [] dans Buffer dans System.Core / System / Linq / Enumerable.cs comme une copie? Et si vous appelez ToList sur IEnumerable, vous pourriez avoir momentanément 3 copies en mémoire à la fois. C'est un problème pour les très grands tableaux, ce qui faisait partie de mon argument. De plus, si vous avez besoin du même ordre de tri plusieurs fois, appeler une fois Trier sur place est beaucoup plus efficace que de trier à plusieurs reprises la liste, en raison de sa permanence.
PRMan
1
@PRMan Oh, vous vouliez dire qu'une copie triée est construite en interne. C'est toujours inexact, car OrderBy ne crée pas la copie - d'après ce que je peux voir, cela est fait par la méthode GetEnumerator lorsque vous commencez réellement à parcourir la collection. J'ai juste essayé de parcourir mon code et j'ai constaté que le code qui remplit une variable d'une expression LINQ s'exécute presque instantanément, mais lorsque vous entrez dans la boucle foreach, il passe du temps à le trier. Je suppose que lorsque j'aurai un peu plus de temps, je devrais passer un peu à essayer de comprendre comment cela fonctionne dans les coulisses.
Stewart

Réponses:

90

Pourquoi ne pas le mesurer:

class Program
{
    class NameComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return string.Compare(x, y, true);
        }
    }

    class Person
    {
        public Person(string id, string name)
        {
            Id = id;
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
    }

    static void Main()
    {
        List<Person> persons = new List<Person>();
        persons.Add(new Person("P005", "Janson"));
        persons.Add(new Person("P002", "Aravind"));
        persons.Add(new Person("P007", "Kazhal"));

        Sort(persons);
        OrderBy(persons);

        const int COUNT = 1000000;
        Stopwatch watch = Stopwatch.StartNew();
        for (int i = 0; i < COUNT; i++)
        {
            Sort(persons);
        }
        watch.Stop();
        Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

        watch = Stopwatch.StartNew();
        for (int i = 0; i < COUNT; i++)
        {
            OrderBy(persons);
        }
        watch.Stop();
        Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);
    }

    static void Sort(List<Person> list)
    {
        list.Sort((p1, p2) => string.Compare(p1.Name, p2.Name, true));
    }

    static void OrderBy(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToArray();
    }
}

Sur mon ordinateur, une fois compilé en mode Release, ce programme imprime:

Sort: 1162ms
OrderBy: 1269ms

METTRE À JOUR:

Comme suggéré par @Stefan, voici les résultats du tri d'une grande liste moins de fois:

List<Person> persons = new List<Person>();
for (int i = 0; i < 100000; i++)
{
    persons.Add(new Person("P" + i.ToString(), "Janson" + i.ToString()));
}

Sort(persons);
OrderBy(persons);

const int COUNT = 30;
Stopwatch watch = Stopwatch.StartNew();
for (int i = 0; i < COUNT; i++)
{
    Sort(persons);
}
watch.Stop();
Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

watch = Stopwatch.StartNew();
for (int i = 0; i < COUNT; i++)
{
    OrderBy(persons);
}
watch.Stop();
Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);

Impressions:

Sort: 8965ms
OrderBy: 8460ms

Dans ce scénario, il semble que OrderBy fonctionne mieux.


UPDATE2:

Et en utilisant des noms aléatoires:

List<Person> persons = new List<Person>();
for (int i = 0; i < 100000; i++)
{
    persons.Add(new Person("P" + i.ToString(), RandomString(5, true)));
}

Où:

private static Random randomSeed = new Random();
public static string RandomString(int size, bool lowerCase)
{
    var sb = new StringBuilder(size);
    int start = (lowerCase) ? 97 : 65;
    for (int i = 0; i < size; i++)
    {
        sb.Append((char)(26 * randomSeed.NextDouble() + start));
    }
    return sb.ToString();
}

Rendements:

Sort: 8968ms
OrderBy: 8728ms

Still OrderBy est plus rapide

Darin Dimitrov
la source
2
Je pense que c'est très différent de trier une très petite liste (3 éléments) 1000000 fois, ou de trier une très grande liste (1000000 éléments) quelques fois seulement. Les deux sont très pertinents. En pratique, la taille moyenne de la liste (qu'est-ce que c'est moyen? ... disons 1000 éléments pour l'instant) est la plus intéressante. IMHO, le tri des listes avec 3 éléments n'est pas très significatif.
Stefan Steinegger
25
Notez qu'il existe une différence entre "plus rapide" et "sensiblement plus rapide". Dans votre dernier exemple, la différence était d'environ un quart de seconde. L'utilisateur va-t-il le remarquer? Est-il inacceptable pour l'utilisateur d'attendre près de neuf secondes pour le résultat? Si les réponses aux deux questions sont «non», alors peu importe celle que vous choisissez du point de vue de la performance.
Eric Lippert
12
Notez également que le test trie ici la liste avant de démarrer le chronomètre, nous comparons donc la façon dont les deux algorithmes se comparent face à une entrée triée. Cela peut être très différent de leurs performances relatives avec une entrée non triée.
phoog
3
Ces résultats sont assez surprenants à mon humble avis, compte tenu du fait que LINQdoit dépenser de la mémoire supplémentaire par rapport à une mise en List<T>.Sortœuvre en place . Je ne suis pas sûr qu'ils aient amélioré cela dans les nouvelles versions .NET, mais sur ma machine (version i7 3ème génération 64 bits .NET 4.5), les Sortperformances sont supérieures OrderBydans tous les cas. De plus, en regardant OrderedEnumerable<T>le code source, il semble qu'il crée trois tableaux supplémentaires (d'abord a Buffer<T>, puis un tableau de clés projetées, puis un tableau d'indices) avant d'appeler enfin Quicksort pour trier le tableau d'indices en place.
Groo
2
... et après tout cela, il y a l' ToArrayappel qui crée le tableau résultant. Les opérations de mémoire et l'indexation de tableau sont des opérations incroyablement rapides, mais je ne trouve toujours pas la logique derrière ces résultats.
Groo
121

Non, ce n'est pas le même algorithme. Pour commencer, le LINQ OrderByest documenté comme stable (c'est-à-dire que si deux éléments ont le même Name, ils apparaîtront dans leur ordre d'origine).

Cela dépend également de la mise en mémoire tampon de la requête ou de l'itération plusieurs fois (LINQ-to-Objects, à moins que vous ne tamponniez le résultat, sera réorganisé par foreach).

Pour la OrderByrequête, je serais également tenté d'utiliser:

OrderBy(n => n.Name, StringComparer.{yourchoice}IgnoreCase);

(pour l' {yourchoice}un des CurrentCulture, Ordinalou InvariantCulture).

List<T>.Sort

Cette méthode utilise Array.Sort, qui utilise l'algorithme QuickSort. Cette implémentation effectue un tri instable; autrement dit, si deux éléments sont égaux, leur ordre peut ne pas être conservé. En revanche, un tri stable préserve l'ordre des éléments égaux.

Enumerable.OrderBy

Cette méthode effectue un tri stable; autrement dit, si les clés de deux éléments sont égales, l'ordre des éléments est conservé. En revanche, un tri instable ne préserve pas l'ordre des éléments qui ont la même clé. Trier; autrement dit, si deux éléments sont égaux, leur ordre peut ne pas être conservé. En revanche, un tri stable préserve l'ordre des éléments égaux.

Marc Gravell
la source
5
Si vous utilisez .NET Reflector ou ILSpy pour ouvrir Enumerable.OrderByet explorer son implémentation interne, vous pouvez voir que l'algorithme de tri OrderBy est une variante de QuickSort qui effectue un tri stable. (Voir System.Linq.EnumerableSorter<TElement>). Ainsi, Array.Sortet Enumerable.OrderBypeut à la fois s'attendre à ce que O (N log N) temps d'exécution, où N est le nombre d'éléments de la collection.
John Beyer
@Marc Je ne comprends pas vraiment quelle serait la différence si deux éléments étaient égaux et leur ordre n'était pas préservé. Cela ne semble certainement pas être un problème pour les types de données primitifs. Mais même pour un type de référence, pourquoi serait-il important, si je devais trier, qu'une personne du nom Marc Gravell soit apparue devant une autre personne du nom Marc Gravell (par exemple :))? Je ne remets pas en question votre réponse / vos connaissances, je recherche plutôt une application de ce scénario.
Mukus
4
@Mukus imagine que vous triez un carnet d'adresses d'entreprise par nom (ou même par date de naissance) - il y aura inévitablement des doublons. La question est finalement: que se passe-t-il pour eux? Le sous-ordre est-il défini?
Marc Gravell
55

La réponse de Darin Dimitrov montre que OrderByc'est légèrement plus rapide que List.Sortface à des entrées déjà triées. J'ai modifié son code pour qu'il trie à plusieurs reprises les données non triées, et OrderByest dans la plupart des cas légèrement plus lent.

De plus, le OrderBytest utilise ToArraypour forcer l'énumération de l'énumérateur Linq, mais cela renvoie évidemment un type ( Person[]) qui est différent du type d'entrée ( List<Person>). J'ai donc relancé le test en utilisant ToListplutôt que ToArrayet j'ai obtenu une différence encore plus grande:

Sort: 25175ms
OrderBy: 30259ms
OrderByWithToList: 31458ms

Le code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

class Program
{
    class NameComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return string.Compare(x, y, true);
        }
    }

    class Person
    {
        public Person(string id, string name)
        {
            Id = id;
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
        public override string ToString()
        {
            return Id + ": " + Name;
        }
    }

    private static Random randomSeed = new Random();
    public static string RandomString(int size, bool lowerCase)
    {
        var sb = new StringBuilder(size);
        int start = (lowerCase) ? 97 : 65;
        for (int i = 0; i < size; i++)
        {
            sb.Append((char)(26 * randomSeed.NextDouble() + start));
        }
        return sb.ToString();
    }

    private class PersonList : List<Person>
    {
        public PersonList(IEnumerable<Person> persons)
           : base(persons)
        {
        }

        public PersonList()
        {
        }

        public override string ToString()
        {
            var names = Math.Min(Count, 5);
            var builder = new StringBuilder();
            for (var i = 0; i < names; i++)
                builder.Append(this[i]).Append(", ");
            return builder.ToString();
        }
    }

    static void Main()
    {
        var persons = new PersonList();
        for (int i = 0; i < 100000; i++)
        {
            persons.Add(new Person("P" + i.ToString(), RandomString(5, true)));
        } 

        var unsortedPersons = new PersonList(persons);

        const int COUNT = 30;
        Stopwatch watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            Sort(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("Sort: {0}ms", watch.ElapsedMilliseconds);

        watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            OrderBy(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("OrderBy: {0}ms", watch.ElapsedMilliseconds);

        watch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            watch.Start();
            OrderByWithToList(persons);
            watch.Stop();
            persons.Clear();
            persons.AddRange(unsortedPersons);
        }
        Console.WriteLine("OrderByWithToList: {0}ms", watch.ElapsedMilliseconds);
    }

    static void Sort(List<Person> list)
    {
        list.Sort((p1, p2) => string.Compare(p1.Name, p2.Name, true));
    }

    static void OrderBy(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToArray();
    }

    static void OrderByWithToList(List<Person> list)
    {
        var result = list.OrderBy(n => n.Name, new NameComparer()).ToList();
    }
}
phoog
la source
2
J'exécute le code de test maintenant dans LinqPad 5 (.net 5) et OrderByWithToListprend le même temps que OrderBy.
dovid
38

Je pense qu'il est important de noter une autre différence entre Sortet OrderBy:

Supposons qu'il existe un Person.CalculateSalary() méthode qui prend beaucoup de temps; peut-être même plus que l'opération consistant à trier une grande liste.

Comparer

// Option 1
persons.Sort((p1, p2) => Compare(p1.CalculateSalary(), p2.CalculateSalary()));
// Option 2
var query = persons.OrderBy(p => p.CalculateSalary()); 

L'option 2 peut avoir des performances supérieures, car elle n'appelle la CalculateSalaryméthode que n fois, alors que l' Sortoption peut appeler CalculateSalaryjusqu'à 2 n fois log ( n ) , en fonction du succès de l'algorithme de tri.

Omer Raviv
la source
4
Cela est vrai, bien qu'il existe une solution à ce problème, à savoir, conserver les données dans un tableau et utiliser la surcharge Array.Sort qui prend deux tableaux, l'un de clés et l'autre de valeurs. En remplissant le tableau de clés, vous appellerez CalculateSalary ntimes. Ce n'est évidemment pas aussi pratique que d'utiliser OrderBy.
phoog
14

En un mot :

Trier par liste / tableau ():

  • Tri instable.
  • Fait sur place.
  • Utilisez Introsort / Quicksort.
  • La comparaison personnalisée est effectuée en fournissant un comparateur. Si la comparaison est coûteuse, elle peut être plus lente que OrderBy () (qui permet d'utiliser des clés, voir ci-dessous).

OrderBy / ThenBy ():

  • Tri stable.
  • Pas en place.
  • Utilisez Quicksort. Quicksort n'est pas une sorte stable. Voici l'astuce: lors du tri, si deux éléments ont la même clé, il compare leur ordre initial (qui a été stocké avant le tri).
  • Permet d'utiliser des clés (en utilisant des lambdas) pour trier les éléments sur leurs valeurs (par exemple:) x => x.Id. Toutes les clés sont extraites avant le tri. Cela peut entraîner de meilleures performances que l'utilisation de Sort () et d'un comparateur personnalisé.

Sources: MDSN , source de référence et référentiel dotnet / coreclr (GitHub).

Certaines des instructions répertoriées ci-dessus sont basées sur l'implémentation actuelle du framework .NET (4.7.2). Cela pourrait changer à l'avenir.

tigrou
la source
0

vous devez calculer la complexité des algorithmes utilisés par les méthodes OrderBy et Sort. QuickSort a une complexité de n (log n) comme je me souviens, où n est la longueur du tableau.

J'ai aussi recherché orderby, mais je n'ai trouvé aucune information, même dans la bibliothèque msdn. si vous n'avez pas les mêmes valeurs et le tri lié à une seule propriété, je préfère utiliser la méthode Sort (); sinon, utilisez OrderBy.

icaptan
la source
1
Selon la documentation MSDN actuelle, Sort utilise 3 algorithmes de tri différents basés sur l'entrée. Parmi eux, QuickSort. La question sur l'algorithme OrderBy () est ici (Quicksort): stackoverflow.com/questions/2792074/…
Thor
-1

Je veux juste ajouter que orderby est bien plus utile.

Pourquoi? Parce que je peux faire ceci:

Dim thisAccountBalances = account.DictOfBalances.Values.ToList
thisAccountBalances.ForEach(Sub(x) x.computeBalanceOtherFactors())
thisAccountBalances=thisAccountBalances.OrderBy(Function(x) x.TotalBalance).tolist
listOfBalances.AddRange(thisAccountBalances)

Pourquoi un comparateur compliqué? Triez simplement en fonction d'un champ. Ici, je trie en fonction de TotalBalance.

Très facile.

Je ne peux pas faire ça avec tri. Je me demande pourquoi. Faites bien avec orderBy.

Quant à la vitesse, c'est toujours O (n).

user4951
la source
3
Question: L'heure O (n) (je suppose) dans votre réponse fait référence à OrderBy ou Comparer? Je ne pense pas que le tri rapide puisse atteindre le temps O (N).
Kevman