【发布时间】:2011-10-31 22:02:23
【问题描述】:
我觉得标题很清楚。
我想知道在Dictionary<K,V> 中使用IEqualityComparer 时是否会产生一定的效率开销,提供一个时它是如何工作的?
谢谢
【问题讨论】:
标签: c# .net performance dictionary
我觉得标题很清楚。
我想知道在Dictionary<K,V> 中使用IEqualityComparer 时是否会产生一定的效率开销,提供一个时它是如何工作的?
谢谢
【问题讨论】:
标签: c# .net performance dictionary
更快吗?
从游戏开发的角度来看,如果您的键是值类型(结构、原始、枚举等),则提供您自己的 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<T>)。但是,在这些情况下发生装箱时有许多令人惊讶的规则,因此我建议尽可能使用配对接口(例如,在这种情况下为 IEqualityComparer<T>)。
【讨论】:
EqualityComparer<T>.Default 首先检查类型是否实现IEquatable<T>,如果是,则使用实现;这意味着如果您的值类型实现 IEquatable<T> 接口,您不必必须提供自定义比较器来避免装箱。
Foo(IEquatable<int> obj) 会将其装箱,但调用 Bar<T>(T obj) where T : IEquatable<int> 不会。这就是泛型存在的原因。
Object.Equals (see IL000E),而不是 IEquatable<T>.Equals。编译器如何知道 obj1 或 obj2 是否是 MyEquatableThing,其签名如下:bool Bar<T>(T obj1, T obj2) where T : IEquatable<MyEquatableThing>?将您的泛型类型约束更改为where T : IEquatable<T> 并再次检查IL,没有box 指令。
Jonathan 有一个 great answer,它指出了如何使用正确的相等比较器来提高性能,并且 Jon 在 his great answer 中澄清说,Dictionary<K, V> 始终使用 IEqualityComparer<T>,即 EqualityComparer<T>.Default,除非您指定另一个。
我想谈谈IEquatable<T> 接口在使用默认相等比较器时的作用。
当您调用EqualityComparer<T>.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<T> 的类型意味着什么?
这里,GenericEqualityComparer<T>的定义:
internal class GenericEqualityComparer<T> : EqualityComparer<T>
where T: IEquatable<T>
// ...
魔术发生在泛型类型约束(where T : IEquatable<T> 部分)中,因为如果T 是值类型,则使用它不涉及装箱,这里不会发生像(IEquatable<T>)T 这样的强制转换,这是泛型的主要好处。
所以,假设我们想要一个将整数映射到字符串的字典。
如果我们使用默认构造函数初始化一个会发生什么?
var dict = new Dictionary<int, string>();
EqualityComparer<T>.Default,除非我们指定另一个。EqualityComparer<int>.Default 会检查int 是否实现IEquatable<int>。int (Int32) 实现了IEquatable<Int32>。第一次调用EqualityComparer<T>.Default 将创建并缓存一个通用比较器,这可能需要一点时间,但在初始化时,它是一个强类型GenericEqualityComparer<T>,使用它不会导致任何装箱或不必要的开销。
并且所有后续对EqualityComparer<T>.Default的调用都将返回缓存的比较器,这意味着初始化的开销对于每种类型都是一次性的。
那么这一切意味着什么?
T 没有实现IEquatable<T> 或它对IEquatable<T> 的实现没有按照您的意愿执行,请实现自定义相等比较器。obj1.Equals(obj2) 不会给你想要的结果。)在 Jonathan 的回答中使用 StringComparer 是一个很好的例子,为什么您要指定自定义相等比较器。
T 实现IEquatable<T> 并且 IEquatable<T> 的实现符合您的要求,不要实现自定义相等比较器。
(即obj1.Equals(obj2) 为您提供所需的结果)。在后一种情况下,请改用EqualityComparer<T>.Default。
【讨论】:
Dictionary<,> 总是使用IEqualityComparer<TKey> - 如果你没有通过,它使用EqualityComparer<T>.Default。因此效率将取决于您的实现与EqualityComparer<T>.Default(仅代表Equals 和GetHashCode)相比的效率。
【讨论】:
我在制作相同的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;
}
}
【讨论】: