为何使用自定义比较器的OrderBy性能远低于默认实现?
我原本以为两种排序逻辑本质相同,性能应该相近,但测试结果显示差异极大。想请教为什么使用自定义IComparer<Row>的排序版本(Sort2)性能远低于直接通过OrderBy指定Key的版本(Sort1)?难道是因为自定义比较器版本需要调用Compare方法和double.CompareTo,而Sort1仅调用double.CompareTo?但这点差异真的会导致如此大的性能差距吗?
public class Program { public class ColumnComparer : IComparer<Row> { public ColumnComparer(int column) { Column = column; } public int Column { get; } public int Compare(Row x, Row y) => x.Values[Column].CompareTo(y.Values[Column]); } public class Row { public int Index { get; } public List<double> Values { get; } = new(); public Row(int index) { Index = index; } } public class Sort { private readonly List<Row> _rows; public Sort() { _rows = new List<Row>(); var r = new Random(0); for (int i = 0; i < 500000; ++i) { var row = new Row(i); for (int j = 0; j < 32; ++j) { row.Values.Add(r.Next(0, 1000)); } _rows.Add(row); } } [Benchmark] public void Sort1() { _rows.OrderBy(x => x.Values[8]).ToArray(); } [Benchmark] public void Sort2() { var comparer = new ColumnComparer(8); var sort2 = _rows.OrderBy(e => e, comparer).ToArray(); } } static void Main(string[] args) { BenchmarkRunner.Run<Sort>(); } }
性能测试结果
初始测试数据
| Method | Mean | Error | StdDev |
|---|---|---|---|
| Sort1 | 111.6 ms | 1.46 ms | 1.79 ms |
| Sort2 | 343.5 ms | 4.01 ms | 7.42 ms |
补充Sort3后的测试数据
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| Sort1 | 112.0 ms | 0.90 ms | 0.85 ms | 13.35 MB |
| Sort2 | 344.4 ms | 2.27 ms | 2.12 ms | 13.35 MB |
| Sort3 | 138.7 ms | 0.64 ms | 0.57 ms | 17.17 MB |
核心原因分析
两者性能差距的本质在于LINQ OrderBy不同重载的实现逻辑差异:
Sort1的
OrderBy(x => x.Values[8])实现逻辑
这个重载接收KeySelector委托,会先遍历所有Row元素,一次性提取并缓存所有需要排序的Key(即x.Values[8]的double值),存储到临时数组中。后续排序过程中,仅需直接比较临时数组里的预存Key,无需再访问Row对象的Values列表。这种方式下每个Row的目标值只被读取一次,排序时的比较操作仅涉及简单的double.CompareTo,开销极低。Sort2的
OrderBy(e => e, comparer)实现逻辑
这个重载接收IComparer<Row>,不会预提取或缓存排序Key。每次比较两个Row对象时,都要调用自定义比较器的Compare方法,内部需要两次访问List<double>的索引,再调用double.CompareTo完成比较。排序的比较次数是O(n log n)级别(50万元素总比较次数约950万次),每次比较都重复上述操作,额外的索引访问和方法调用开销累加后,总耗时达到Sort1的3倍左右。关于Sort3的补充
Sort3耗时介于两者之间,结合内存分配更高的特点,推测它是手动预提取Key(比如将Row和对应Key封装成Tuple)后再排序,这种方式避免了重复访问Values,但额外的对象封装带来少量开销,因此性能略逊于Sort1,但远优于Sort2。
内容的提问来源于stack exchange,提问作者volvic

