C#Sort和OrderBy比较

use*_*675 98 .net c# sorting performance sql-order-by

我可以使用Sort或OrderBy对列表进行排序.哪一个更快?两者都在使用相同的算法吗?

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

1.

persons.Sort((p1,p2)=>string.Compare(p1.Name,p2.Name,true));
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*ell 120

不,它们不是同一种算法.对于初学者来说,LINQ OrderBy被记录为稳定(即如果两个项目相同Name,它们将以原始顺序出现).

它还取决于您是否缓冲查询与多次迭代(LINQ-to-Objects,除非您缓冲结果,将重新排序foreach).

对于OrderBy查询,我也很想使用:

OrderBy(n => n.Name, StringComparer.{yourchoice}IgnoreCase);
Run Code Online (Sandbox Code Playgroud)

(对于{yourchoice}其中之一CurrentCulture,OrdinalInvariantCulture).

List<T>.Sort

此方法使用Array.Sort,它使用QuickSort算法.此实现执行不稳定的排序; 也就是说,如果两个元素相等,则可能不会保留它们的顺序.相反,稳定的排序保留了相等元素的顺序.

Enumerable.OrderBy

该方法执行稳定的排序; 也就是说,如果两个元素的键相等,则保留元素的顺序.相反,不稳定的排序不会保留具有相同键的元素的顺序.分类; 也就是说,如果两个元素相等,则可能不会保留它们的顺序.相反,稳定的排序保留了相等元素的顺序.

  • 如果您使用.NET Reflector或ILSpy来破解"Enumerable.OrderBy"并深入了解其内部实现,您可以看到OrderBy排序算法是QuickSort的一种变体,可以进行稳定的排序.(参见`System.Linq.EnumerableSorter <TElement>`.)因此,`Array.Sort`和`Enumerable.OrderBy`都可以预期有_O(N log N)_执行时间,其中_N_是元素的数量在集合中. (5认同)
  • @Mukus想象你按名称(或者确实按出生日期)对公司地址簿进行排序 - 不可避免地会出现重复.最终的问题是:他们会发生什么?是否定义了子订单? (4认同)

Dar*_*rov 88

为什么不测量它:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

在发布模式下编译时,我的计算机上打印:

Sort: 1162ms
OrderBy: 1269ms
Run Code Online (Sandbox Code Playgroud)

更新:

正如@Stefan所建议的那样,对大型列表进行较少次排序的结果如下:

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);
Run Code Online (Sandbox Code Playgroud)

打印:

Sort: 8965ms
OrderBy: 8460ms
Run Code Online (Sandbox Code Playgroud)

在这种情况下,看起来OrderBy表现更好.


UPDATE2:

并使用随机名称:

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

哪里:

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();
}
Run Code Online (Sandbox Code Playgroud)

产量:

Sort: 8968ms
OrderBy: 8728ms
Run Code Online (Sandbox Code Playgroud)

仍然OrderBy更快

  • 请注意,"更快"和"明显更快"之间存在差异.在你的最后一个例子中,差异大约是四分之一秒.用户会注意到吗?用户等待将近9秒才能获得结果是不可接受的吗?如果这两个问题的答案都是"不",那么从绩效角度来看,选择哪一个并不重要. (23认同)
  • 另请注意,此处的测试在启动秒表之前对列表进行排序,因此我们将比较两种算法在面对排序输入时的比较方式.这可能与它们与未分类输入的相对性能完全不同. (12认同)
  • 考虑到`LINQ`与就地`List <T> .Sort`实现相比需要花费额外的内存,这些结果非常令人惊讶.我不确定他们是否在较新的.NET版本中改进了这一点,但在我的机器上(i7 3rd gen 64-bit .NET 4.5发行版)`Sort`在所有情况下都优于`OrderBy`.此外,通过查看`OrderedEnumerable <T>`源代码,似乎它创建了**三个**附加数组(首先是一个`Buffer <T>`,然后是一个投影键数组,然后是一个索引数组)最后调用Quicksort对索引数组进行排序. (3认同)
  • 我认为,将一个非常小的列表(3个项目)排序1000000次,或者通过对非常大的列表(1000000个项目)进行排序只需几次就可以了.两者都非常相关.在实践中,中等大小的列表(什么是中等?...现在说1000个项目)是最有趣的.恕我直言,有3个项目的排序列表不是很有意义. (2认同)
  • ...然后在所有这些之后,有一个`ToArray`调用,它创建了结果数组.内存操作和数组索引是非常快速的操作,但我仍然无法找到这些结果背后的逻辑. (2认同)
  • 我认为你的 `Sort` 方法是有问题的,因为它改变了提供的 `List&lt;Person&gt;`,这使得未来的排序更快,因为列表已经排序。我建议为每次迭代创建一个新的随机字符串列表,以便结果的分布更加均匀。 (2认同)

pho*_*oog 55

Darin Dimitrov的回答表明,这OrderByList.Sort面对已经排序的输入要快一些.我修改了他的代码,因此它重复排序未排序的数据,并且OrderBy在大多数情况下稍慢.

此外,OrderBy测试用于ToArray强制Linq枚举器的枚举,但这显然会返回一个Person[]与输入类型(List<Person>)不同的type().因此,我使用ToList而不是ToArray获得更大的差异重新进行测试:

Sort: 25175ms
OrderBy: 30259ms
OrderByWithToList: 31458ms
Run Code Online (Sandbox Code Playgroud)

代码:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我现在在LinqPad 5(.net 5)中运行测试代码,并且`OrderByWithToList`与`OrderBy`的时间相同. (2认同)

Ome*_*viv 35

我认为重要的是要注意Sort和之间的另一个区别OrderBy:

假设存在一种Person.CalculateSalary()花费大量时间的方法; 可能甚至超过排序大型列表的操作.

相比

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

选项2可能具有优越的性能,因为它只调用CalculateSalary方法n次,而Sort选项可能CalculateSalary最多调用2 n log(n)次,具体取决于排序算法的成功.

  • 这是事实,尽管有一个解决方案可以解决这个问题,即将数据保存在一个数组中并使用带有两个数组的Array.Sort重载,一个是键,另一个是值.在填充键数组时,您将调用CalculateSalary`n`次.这显然不如使用OrderBy那么方便. (4认同)

tig*_*rou 10

简而言之 :

List/Array Sort():

  • 不稳定的排序.
  • 就地完成.
  • 使用Introsort/Quicksort.
  • 通过提供比较器来完成自定义比较.如果比较昂贵,它可能比OrderBy()慢(允许使用键,见下文).

OrderBy/ThenBy():

  • 稳定的排序.
  • 不到位.
  • 使用Quicksort.Quicksort不是一个稳定的类型.这是诀窍:当排序时,如果两个元素具有相同的键,则比较它们的初始顺序(在排序之前已经存储).
  • 允许使用键(使用lambdas)对其值上的元素进行排序(例如:) x => x.Id.在排序之前首先提取所有键.这可能会比使用Sort()和自定义比较器产生更好的性能.

来源: MDSN,参考源dotnet/coreclr存储库(GitHub).

上面列出的一些语句基于当前的.NET框架实现(4.7.2).它可能会在未来发生变化.