【问题标题】:Efficiency of using IEqualityComparer in Dictionary vs HashCode and Equals()在 Dictionary vs HashCode 和 Equals() 中使用 IEqualityComparer 的效率
【发布时间】:2011-10-31 22:02:23
【问题描述】:

我觉得标题很清楚。

我想知道在Dictionary<K,V> 中使用IEqualityComparer 时是否会产生一定的效率开销,提供一个时它是如何工作的?

谢谢

【问题讨论】:

    标签: c# .net performance dictionary


    【解决方案1】:

    更快吗?

    从游戏开发的角度来看,如果您的键是值类型(结构、原始、枚举等),则提供您自己的 EqualityComparer<T> 会明显更快 - 因为 EqualityComparer<T>.Default 将值框起来。

    作为一个真实的示例,托管 DirectX 广告牌示例过去的运行速度约为 C++ 版本的 30%;其中所有其他样本的运行速度约为 90%。这样做的原因是广告牌是使用默认比较器进行排序的(因此被装箱),结果证明每帧复制了 4MB 的数据。

    它是如何工作的?

    Dictionary<K,V> 将通过默认构造函数为自己提供EqualityComparer<T>.Default。默认相等比较器所做的是(基本上,注意发生了多少装箱):

    public void GetHashCode(T value)
    {
       return ((object)value).GetHashCode();
    }
    
    public void Equals(T first, T second)
    {
       return ((object)first).Equals((object)second);
    }
    

    我为什么要使用它?

    这种代码很常见(尝试使用不区分大小写的键时):

    var dict = new Dictionary<string, int>();
    dict.Add(myParam.ToUpperInvariant(), fooParam);
    // ...
    var val = dict[myParam.ToUpperInvariant()];
    

    这真的很浪费,最好在构造函数上使用一个StringComparer:

    var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
    

    是否更快(redux)?

    在这种特定情况下,它要快得多,因为序数字符串比较是您可以进行的最快的字符串比较类型。快速基准测试:

    static void Main(string[] args)
    {
        var d1 = new Dictionary<string, int>();
        var d2 = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
    
        d1.Add("FOO", 1);
        d2.Add("FOO", 1);
    
        Stopwatch s = new Stopwatch();
        s.Start();
        RunTest1(d1, "foo");
        s.Stop();
        Console.WriteLine("ToUpperInvariant: {0}", s.Elapsed);
    
        s.Reset();
        s.Start();
        RunTest2(d2, "foo");
        s.Stop();
        Console.WriteLine("OrdinalIgnoreCase: {0}", s.Elapsed);
    
        Console.ReadLine();
    }
    
    static void RunTest1(Dictionary<string, int> values, string val)
    {
        for (var i = 0; i < 10000000; i++)
        {
            values[val.ToUpperInvariant()] = values[val.ToUpperInvariant()];
        }
    }
    
    static void RunTest2(Dictionary<string, int> values, string val)
    {
        for (var i = 0; i < 10000000; i++)
        {
            values[val] = values[val];
        }
    }
    
    // ToUpperInvariant: 00:00:04.5084119
    // OrdinalIgnoreCase: 00:00:02.1211549
    // 2x faster.
    

    预订

    可以通过在结构上实现接口来消除装箱开销(例如IEquatable&lt;T&gt;)。但是,在这些情况下发生装箱时有许多令人惊讶的规则,因此我建议尽可能使用配对接口(例如,在这种情况下为 IEqualityComparer&lt;T&gt;)。

    【讨论】:

    • 很好的答案,但我认为您应该提到EqualityComparer&lt;T&gt;.Default 首先检查类型是否实现IEquatable&lt;T&gt;,如果是,则使用实现;这意味着如果您的值类型实现 IEquatable&lt;T&gt; 接口,您不必必须提供自定义比较器来避免装箱。
    • @ŞafakGür 使用接口访问值类型会将它们装箱:stackoverflow.com/questions/7995606/…
    • 使用 int 调用 Foo(IEquatable&lt;int&gt; obj) 会将其装箱,但调用 Bar&lt;T&gt;(T obj) where T : IEquatable&lt;int&gt; 不会。这就是泛型存在的原因。
    • @ŞafakGür 不。编译它并检查 IL - 通过接口访问结构方法将框 - 原因是结构缺少 ITable+VTable。这就是 IEqualityComparer 存在的原因。我为你省了麻烦:gist.github.com/jcdickinson/9051956#file-program-bar-il-L16(IL0003 是一个盒子指令)
    • 它将结构装箱,因为您的示例调用 Object.Equals (see IL000E),而不是 IEquatable&lt;T&gt;.Equals。编译器如何知道 obj1 或 obj2 是否是 MyEquatableThing,其签名如下:bool Bar&lt;T&gt;(T obj1, T obj2) where T : IEquatable&lt;MyEquatableThing&gt;?将您的泛型类型约束更改为where T : IEquatable&lt;T&gt; 并再次检查IL,没有box 指令。
    【解决方案2】:

    Jonathan 有一个 great answer,它指出了如何使用正确的相等比较器来提高性能,并且 Jon 在 his great answer 中澄清说,Dictionary&lt;K, V&gt; 始终使用 IEqualityComparer&lt;T&gt;,即 EqualityComparer&lt;T&gt;.Default,除非您指定另一个。

    我想谈谈IEquatable&lt;T&gt; 接口在使用默认相等比较器时的作用。

    当您调用EqualityComparer&lt;T&gt;.Default 时,它会使用缓存的比较器(如果有的话)。如果这是您第一次使用该类型的默认相等比较器,它会调用一个名为 CreateComparer 的方法并缓存结果以供以后使用。这是 .NET 4.5 中 CreateComparer 的精简和简化实现:

    var t = (RuntimeType)typeof(T);
    
    // If T is byte,
    // return a ByteEqualityComparer.
    
    // If T implements IEquatable<T>,
    if (typeof(IEquatable<T>).IsAssignableFrom(t))
        return (EqualityComparer<T>)
               RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter(
                   (RuntimeType)typeof(GenericEqualityComparer<int>), t);
    
    // If T is a Nullable<U> where U implements IEquatable<U>,
    // return a NullableEqualityComparer<U>
    
    // If T is an int-based Enum,
    // return an EnumEqualityComparer<T>
    
    // Otherwise return an ObjectEqualityComparer<T>
    

    但是对于实现 IEquatable&lt;T&gt; 的类型意味着什么?
    这里,GenericEqualityComparer&lt;T&gt;的定义:

    internal class GenericEqualityComparer<T> : EqualityComparer<T>
        where T: IEquatable<T>
    // ...
    

    魔术发生在泛型类型约束(where T : IEquatable&lt;T&gt; 部分)中,因为如果T 是值类型,则使用它涉及装箱,这里不会发生像(IEquatable&lt;T&gt;)T 这样的强制转换,这是泛型的主要好处。

    所以,假设我们想要一个将整数映射到字符串的字典。
    如果我们使用默认构造函数初始化一个会发生什么?

    var dict = new Dictionary<int, string>();
    
    • 我们知道字典使用EqualityComparer&lt;T&gt;.Default,除非我们指定另一个。
    • 我们知道EqualityComparer&lt;int&gt;.Default 会检查int 是否实现IEquatable&lt;int&gt;
    • 我们知道int (Int32) 实现了IEquatable&lt;Int32&gt;

    第一次调用EqualityComparer&lt;T&gt;.Default 将创建并缓存一个通用比较器,这可能需要一点时间,但在初始化时,它是一个强类型GenericEqualityComparer&lt;T&gt;,使用它不会导致任何装箱或不必要的开销。

    并且所有后续对EqualityComparer&lt;T&gt;.Default的调用都将返回缓存的比较器,这意味着初始化的开销对于每种类型都是一次性的。


    那么这一切意味着什么?

    • 如果T 没有实现IEquatable&lt;T&gt; 它对IEquatable&lt;T&gt; 的实现没有按照您的意愿执行,请实现自定义相等比较器。
      (即obj1.Equals(obj2) 不会给你想要的结果。)

    在 Jonathan 的回答中使用 StringComparer 是一个很好的例子,为什么您要指定自定义相等比较器。

    • 如果T 实现IEquatable&lt;T&gt; 并且 IEquatable&lt;T&gt; 的实现符合您的要求,不要实现自定义相等比较器。 (即obj1.Equals(obj2) 为您提供所需的结果)。

    在后一种情况下,请改用EqualityComparer&lt;T&gt;.Default

    【讨论】:

      【解决方案3】:

      Dictionary&lt;,&gt; 总是使用IEqualityComparer&lt;TKey&gt; - 如果你没有通过,它使用EqualityComparer&lt;T&gt;.Default。因此效率将取决于您的实现与EqualityComparer&lt;T&gt;.Default(仅代表EqualsGetHashCode)相比的效率。

      【讨论】:

      • @jtbandes:如果你看到这个,你能停止更改我的帖子吗?我更喜欢将所有内容都保留为 ASCII...
      • 哎呀,当然。那你可以考虑使用“--”吗?至少在我看来,它更具可读性:)
      【解决方案4】:

      我在制作相同的EqualityComparer 时遇到了巨大的麻烦...关键部分是GetHashCode,它在定位object[] 时会生成重复的键,并且记录超过 20k .. 下面是解决方案

      public class ObJectArrayEqualityComparer : IEqualityComparer<object[]>
      { 
          public bool Equals(object[] x, object[] y)
          {
              if (x.Length != y.Length)
              {
                  return false;
              }
              for (int i = 0; i < x.Length; i++)
              {
                  var tempX = x[i];
                  var tempY = y[i];
                  if ((tempX==null || tempX ==DBNull.Value) 
                      && (tempY == null || tempY == DBNull.Value))
                  {
                      return true;
                  }
      
                  if (!tempX.Equals(tempY) 
                      && !System.Collections.StructuralComparisons.StructuralEqualityComparer.Equals(tempX, tempY))
                  {
                      return false;
                  }
              }
              return true;
          }
      
          public int GetHashCode(object[] obj)
          {
              if (obj.Length == 1)
              {
                  return obj[0].GetHashCode();
              }
      
              int result = 0;
      
              for (int i = 0; i < obj.Length; i++)
              {
                  result = result + (obj[i].GetHashCode() * (65 + i));
              }
      
              return result;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-18
        • 1970-01-01
        • 1970-01-01
        • 2013-11-02
        • 2013-07-28
        • 1970-01-01
        相关资源
        最近更新 更多