【问题标题】:Why is List<T>.Sort using Comparer<int>.Default more than twice as fast as an equivalent custom comparer?为什么 List<T>.Sort using Comparer<int>.Default 是等效自定义比较器的两倍以上?
【发布时间】:2012-07-11 12:04:45
【问题描述】:

结果

使用一千万个随机ints 的列表(每次相同的种子,平均重复 10 次):

listCopy.Sort(Comparer&lt;int&gt;.Default) 需要 314 毫秒

使用

sealed class IntComparer : IComparer<int>
{
  public int Compare(int x, int y)
  {
    return x < y ? -1 : (x == y ? 0 : 1);
  }
}

listCopy.Sort(new IntComparer()) 需要 716 毫秒

一些变化:

  • 使用struct IntComparer 代替sealed class:771 毫秒
  • 使用public int Compare(int x, int y) { return x.CompareTo(y); }:809ms

评论

Comparer&lt;int&gt;.Default 返回一个GenericComparer&lt;int&gt;。根据 dotPeek,我们有:

internal class GenericComparer<T> : Comparer<T> where T : IComparable<T>
{
  public override int Compare(T x, T y)
  {
    if ((object) x != null)
    {
      if ((object) y != null)
        return x.CompareTo(y);
      else
        return 1;
    }
    else
      return (object) y != null ? -1 : 0;
  }

...
}

显然,这不应该比我使用CompareToIntComparer 变体更快。

我没有在ArraySortHelper&lt;T&gt; 中找到任何相关内容,这似乎是List&lt;T&gt;.Sort 的核心。

我只能猜测 JIT 在这里做了一些神奇的特殊情况(将使用 Comparer&lt;int&gt;.Default 的排序替换为不执行任何 IComparer&lt;T&gt;.Compare 调用的专用排序实现,或类似的东西)?

编辑:上面的时间太低了 5.9214729782462845StopwatchTimeSpan 对“滴答声”有不同的定义)。不过不影响重点。

【问题讨论】:

  • 我们能看到表明您实际计时的代码吗?很多“为什么 X 比 Y 快?”问题一开始就存在时间问题。
  • 另外,如果您将 IntComparer 更改为返回 x - y,这对运行时有何影响?
  • 尝试以不同的顺序运行它们,看看是否得到相同的结果
  • @FunctorSalad 你为什么要做GC.Collect? GC.Collect 不是同步的,除非用GC.WaitForPendingFinalizers 调用。如果您不等待终结器,则收集可能仍在您的定时代码的后台进行。
  • @Cameron 返回 x - y 不是一个好主意。你很容易遇到过低的问题;考虑 x = int.MinValue 和 y = int.MaxValue。你最终会返回一个正值而不是一个负值。

标签: c# performance sorting


【解决方案1】:

原因在Reference Source, system/array.cs 源代码文件中显而易见:

   [ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
   public static void Sort<T>(T[] array, int index, int length, System.Collections.Generic.IComparer<T> comparer) {
       // Argument checking code omitted
       //...

       if (length > 1) {
           // <STRIP>
           // TrySZSort is still faster than the generic implementation.
           // The reason is Int32.CompareTo is still expensive than just using "<" or ">".
           // </STRIP>
           if ( comparer == null || comparer == Comparer<T>.Default ) {
               if(TrySZSort(array, null, index, index + length - 1)) {
                   return;
               }
           }

           ArraySortHelper<T>.Default.Sort(array, index, length, comparer);
       }
   }

&lt;STRIP&gt; 标记的注释解释了它,尽管它的英文很烂:) 默认比较器的代码路径通过 TrySZSort(),这是一个在 CLR 中实现并用 C++ 编写的函数。您可以从SSCLI20 获取它的源代码,它在 clr/src/vm/comarrayhelpers.cpp 中实现。它使用名为ArrayHelpers&lt;T&gt;::QuickSort() 的模板类方法。

它通过使用&lt; 操作符获得了速度优势,单个cpu 指令而不是Int32.CompareTo() 所需的10 条指令。或者换句话说,IComparable.CompareTo 被过度指定用于简单排序。

这是一个微优化,.NET Framework 有很多很多。位于依赖链最底部的代码的命运不可避免,Microsoft 永远不能假设他们的代码在客户的应用程序中不会对速度至关重要。

【讨论】:

  • 我现在觉得自己很蠢。实际上,我在 dotPeek 中浏览了该方法(不是参考源);最后一个if ( comparer == null ... 隐藏在一行中,我将其作为另一个输入验证行阅读:)
  • @HansPassant 从 .Net 4.5 Array.Sort 开始使用 IntroSort。那么在这种情况下我们没有使用TrySZSort,因此我的性能不会受到自定义比较器使用的影响,这对吗?
  • @HansPassant 好,我自己回答:不,只有在 TrySZSort 失败时才应用 IntroSort。
  • @HansPassant TrySZSort 也实现了 IntroSort
【解决方案2】:

ILSpy 反编译如下:

    public override int Compare(T x, T y)
    {
        if (x != null)
        {
            if (y != null)
            {
                return x.CompareTo(y);
            }
            return 1;
        }
        else
        {
            if (y != null)
            {
                return -1;
            }
            return 0;
        }
    }

对于值类型,空检查将始终评估为true,因此它们将被优化掉;最终结果将是

public override int Compare(T x, T y)
{
    return x.CompareTo(y);
}

【讨论】:

  • 这似乎无法解释为什么Comparer&lt;int&gt;.Default 更快
  • @FunctorSalad 我刚刚计时了x.CompareTo(y); vs x &lt; y ? -1 : (x == y ? 0 : 1);CompareTo 在发布模式下平均快两倍。由于Comparer&lt;int&gt;.Default 将优化为x.CompareTo(y);,这就是为什么它的速度大约是自定义比较器的两倍。将您的自定义比较器更改为使用 x.CompareTo(y);,您应该会看到相同的性能。
  • @FunctorSalad 尝试用这个替换你的IntComparer 实现,看看差异是否消失。
  • @FunctorSalad 你是对的;我发布该答案时知道它不完整,因为我没有时间更全面地研究它。我还想提请注意您的问题中的猜测(“我只能猜测 JIT 在这里做了一些神奇的特殊情况(将使用 Comparer.Default 的排序替换为不执行任何操作的专用排序实现) IComparer.Compare 调用,或类似的)?”),这显然是正确的,除了特殊大小写是在 BCL 中完成的,而不是由 JIT 完成。
【解决方案3】:

Int32 的默认比较器是 CompareTo(int,int) 方法。您对默认比较器的假设不正确。

IComparable 接口提供强类型比较 用于对通用集合对象的成员进行排序的方法。因为 这通常不直接从开发人员代码中调用。反而, 它由 List.Sort() 和 Add 等方法自动调用。

http://msdn.microsoft.com/en-us/library/4d7sx9hd.aspx。提到的 IComparable 接口定义了 CompareTo 方法。

所以我们应该期望您的比较器的速度大致相同。那么为什么会慢一些呢?如果我们深入研究 .Net 中的 Sort 方法,我们最终会看到这一行:

if ((length > 1) && (((comparer != null) && (comparer != Comparer<T>.Default)) || !TrySZSort(array, null, index, (index + length) - 1)))
{
    ArraySortHelper<T>.Default.Sort(array, index, length, comparer);
}

如果比较器等于该类型的默认比较器,则数组排序将尝试使用内部优化的排序方法。您的比较器不是默认比较器,因此它会跳过优化排序。

【讨论】:

  • 抱歉,您的编辑是正确答案,但我选择了 Hans Passant 的答案,因为它比编辑稍早。关于您的第一点,我在这里看不到我的错误,因为我得出结论Comparer&lt;int&gt;.DefaultGenericComparer&lt;T&gt;,它从ICompareble&lt;T&gt; 调用CompareTo 方法。
  • 没关系,他在我进行编辑时发布,但直到我保存编辑后才看到他的。他的回答很好地解释了这一点(我也赞成)。我将在我的帖子中添加一些关于第一点的内容以便更好地解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-20
  • 1970-01-01
  • 1970-01-01
  • 2011-03-04
  • 1970-01-01
相关资源
最近更新 更多