[英]dictionary enum key performance
我担心使用枚举作为键的通用词典。
如下页所述,对键使用枚举将分配内存:http: //blogs.msdn.com/b/shawnhar/archive/2007/07/02/twin-paths-to-garbage-collector-nirvana.aspx
我已经测试并确认了该行为,它在我的项目中引起了问题。 为了可读性,我相信对键使用枚举非常有用,对我来说最佳解决方案是编写一个实现IDictionary<TKey, TValue>
的类,它将在内部使用整数作为键。 原因是我不想更改所有现有词典以使用整数作为键,并进行隐式转换。 这将是最好的性能明智,但它会给我很多工作,并且会降低可读性。
所以我尝试了几种方法,包括使用GetHashCode
(不幸的是分配内存)来构建内部Dictionary<int, TValue>
。
因此,将其总结为一个问题; 谁能想到一个解决方案,我可以用它来保持Dictionary<SomeEnum, TValue>
的可读性,同时具有Dictionary<int, TValue>
的性能?
非常感谢任何建议。
问题是拳击。 这是将值类型转换为对象的行为,这可能是不必要的,也可能不是。
Dictionary
比较键的方式本质上是,它将使用EqualComparer<T>.Default
,并调用GetHashCode()
来查找正确的存储桶,并使用Equals
来比较存储桶中是否有任何值与我们的值相等寻找。
好消息是:.NET 框架有很好的优化,可以避免在"Enum integers"
的情况下装箱。 请参阅CreateComparer() 。 您在这里看到整数和枚举作为键的任何区别的可能性很小。
这里要注意:这不是一件容易的事,事实上,如果你深入挖掘,你会得出结论,这场战斗的四分之一是通过 CLR“黑客”实现的。 如这里所见:
static internal int UnsafeEnumCast<T>(T val) where T : struct
{
// should be return (int) val; but C# does not allow, runtime
// does this magically
// See getILIntrinsicImplementation for how this happens.
throw new InvalidOperationException();
}
如果泛型有 Enum 约束,甚至可能有很长的行UnsafeEnumCast<T>(T val) where T : Enum->Integer
,那肯定会更容易,但好吧......他们没有。
您可能想知道,该EnumCast
什么? 我也想知道不完全确定在这个正确的时刻如何检查它。 我相信它在运行时被替换为特定的 IL 代码?!
现在,回答你的问题:是的,你是对的。 Enum
作为 Mono 上的键,在紧密循环中会变慢。 据我所知,这是因为 Mono 在枚举上进行拳击。 您可以查看EnumIntEqualityComparer ,如您所见,它调用Array.UnsafeMov
基本上将T
类型转换为整数,通过装箱: (int)(object) instance;
. 这是泛型的“经典”限制,这个问题没有很好的解决方案。
为您的具体枚举实现EqualityComparer<MyEnum>
。 这将避免所有的铸造。
public struct MyEnumCOmparer : IEqualityComparer<MyEnum>
{
public bool Equals(MyEnum x, MyEnum y)
{
return x == y;
}
public int GetHashCode(MyEnum obj)
{
// you need to do some thinking here,
return (int)obj;
}
}
然后,您需要做的就是将其传递给您的Dictionary
:
new Dictionary<MyEnum, int>(new MyEnumComparer());
它有效,它为您提供与整数相同的性能,并避免装箱问题。 问题是,这不是通用的,为每个Enum
编写它会让人觉得很愚蠢。
编写一个通用的Enum
比较器,并使用一些避免拆箱的技巧。 我从这里得到了一点帮助,写了这个,
// todo; check if your TEnum is enum && typeCode == TypeCode.Int
struct FastEnumIntEqualityComparer<TEnum> : IEqualityComparer<TEnum>
where TEnum : struct
{
static class BoxAvoidance
{
static readonly Func<TEnum, int> _wrapper;
public static int ToInt(TEnum enu)
{
return _wrapper(enu);
}
static BoxAvoidance()
{
var p = Expression.Parameter(typeof(TEnum), null);
var c = Expression.ConvertChecked(p, typeof(int));
_wrapper = Expression.Lambda<Func<TEnum, int>>(c, p).Compile();
}
}
public bool Equals(TEnum firstEnum, TEnum secondEnum)
{
return BoxAvoidance.ToInt(firstEnum) ==
BoxAvoidance.ToInt(secondEnum);
}
public int GetHashCode(TEnum firstEnum)
{
return BoxAvoidance.ToInt(firstEnum);
}
}
现在,解决方案#2 有一个小问题,因为Expression.Compile()
在 iOS 上不是那么出名(没有运行时代码生成),而且一些单声道版本没有 ?? Expression.Compile
编译 ?? (没有把握)。
您可以编写简单的 IL 代码来处理枚举转换并编译它。
.assembly extern mscorlib
{
.ver 0:0:0:0
}
.assembly 'enum2int'
{
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.class public auto ansi beforefieldinit EnumInt32ToInt
extends [mscorlib]System.Object
{
.method public hidebysig static int32 Convert<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_000b: ret
}
}
为了将其编译为程序集,您必须调用:
ilasm enum2int.il /dll
其中 enum2int.il 是包含 IL 的文本文件。
您现在可以引用给定的程序集( enum2int.dll
)并调用静态方法,如下所示:
struct FastEnumIntEqualityComparer<TEnum> : IEqualityComparer<TEnum>
where TEnum : struct
{
int ToInt(TEnum en)
{
return EnumInt32ToInt.Convert(en);
}
public bool Equals(TEnum firstEnum, TEnum secondEnum)
{
return ToInt(firstEnum) == ToInt(secondEnum);
}
public int GetHashCode(TEnum firstEnum)
{
return ToInt(firstEnum);
}
}
它可能看起来是杀手级代码,但它避免了装箱,并且应该在Mono
上为您提供更好的性能。
不久前我遇到了同样的问题,最终将它合并到我编写的通用枚举扩展和辅助方法的库中(它是用 C++/CLI(编译的 AnyCPU)编写的,因为 C# 不允许为枚举类型创建类型约束) )。 它在NuGet和GitHub上的 Apache 2.0 许可下可用
您可以通过从库中的静态Enums
类型中获取IEqualityComparer
来在Dictionary
实现它:
var equalityComparer = Enums.EqualityComparer<MyEnum>();
var dictionary = new Dictionary<MyEnum, MyValueType>(equalityComparer);
这些值是在没有装箱的情况下处理的,使用类似于已经提供的一个答案中提到的UnsafeEnumCast
的技术(在测试中被覆盖,因为它是不安全的)。 结果,它非常快(因为在这种情况下这将是替换相等比较器的唯一要点)。 包括一个基准测试应用程序以及我构建的 PC 生成的最新结果。
作为字典键的枚举现在具有与int
字典键相同或更好的性能。 我使用 NUnit 测量了这一点:
public class EnumSpeedTest
{
const int Iterations = 10_000_000;
[Test]
public void WasteTimeInt()
{
Dictionary<int, int> dict = new Dictionary<int, int>();
for (int i = 0; i < Iterations; i++)
dict[i] = i;
long sum = 0;
for (int i = 0; i < Iterations; i++)
sum += dict[i];
Console.WriteLine(sum);
}
enum Enum { Zero = 0, One = 1, Two = 2, Three = 3 }
[Test]
public void WasteTimeEnum()
{
Dictionary<Enum, int> dict = new Dictionary<Enum, int>();
for (int i = 0; i < Iterations; i++)
dict[(Enum)i] = i;
long sum = 0;
for (int i = 0; i < Iterations; i++)
sum += dict[(Enum)i];
Console.WriteLine(sum);
}
}
在我的 Ryzen 5 PC 上的 .NET 5.0 Release 版本中,这两个测试所花费的时间始终在 300 毫秒左右,并且在大多数运行中,枚举版本稍微快一些。
在以后的 .net 版本中(针对 .NET7 测试),性能与使用int
作为键相同。 使用IEqualityComparer
结构作为 Dictionary 的构造函数参数甚至会使性能变差。 作为参考,这里有一些代码显示了一些替代方案和相应的性能。 该代码使用BenchmarkDotNet框架。
public enum MyEnum { Zero = 0, One = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6, Seven = 7, Eight = 8, Nine = 9, Ten = 10 }
public class DictionaryBenchmark
{
const int count = 100;
[Benchmark]
public void Int()
{
Dictionary<int, int> dict = new Dictionary<int, int>();
dict[0] = 0;
dict[1] = 1;
dict[2] = 2;
dict[3] = 3;
dict[4] = 4;
dict[5] = 5;
dict[6] = 6;
dict[7] = 7;
dict[8] = 8;
dict[9] = 9;
dict[10] = 10;
for (int i = 0; i < count; i++)
{
long sum = dict[0] +
dict[1] +
dict[2] +
dict[3] +
dict[4] +
dict[5] +
dict[6] +
dict[7] +
dict[8] +
dict[9] +
dict[10];
}
}
[Benchmark]
public void Enum()
{
Dictionary<MyEnum, int> dict = new Dictionary<MyEnum, int>();
dict[MyEnum.Zero] = 0;
dict[MyEnum.One] = 1;
dict[MyEnum.Two] = 2;
dict[MyEnum.Three] = 3;
dict[MyEnum.Four] = 4;
dict[MyEnum.Five] = 5;
dict[MyEnum.Six] = 6;
dict[MyEnum.Seven] = 7;
dict[MyEnum.Eight] = 8;
dict[MyEnum.Nine] = 9;
dict[MyEnum.Ten] = 10;
for (int i = 0; i < count; i++)
{
long sum = dict[MyEnum.Zero] +
dict[MyEnum.One] +
dict[MyEnum.Two] +
dict[MyEnum.Three] +
dict[MyEnum.Four] +
dict[MyEnum.Five] +
dict[MyEnum.Six] +
dict[MyEnum.Seven] +
dict[MyEnum.Eight] +
dict[MyEnum.Nine] +
dict[MyEnum.Ten];
}
}
struct MyEnumComparer : IEqualityComparer<MyEnum>
{
public bool Equals(MyEnum x, MyEnum y)
{
return x == y;
}
public int GetHashCode(MyEnum obj)
{
return (int)obj;
}
}
[Benchmark]
public void EqualityComparer()
{
Dictionary<MyEnum, int> dict = new Dictionary<MyEnum, int>(new MyEnumComparer());
dict[MyEnum.Zero] = 0;
dict[MyEnum.One] = 1;
dict[MyEnum.Two] = 2;
dict[MyEnum.Three] = 3;
dict[MyEnum.Four] = 4;
dict[MyEnum.Five] = 5;
dict[MyEnum.Six] = 6;
dict[MyEnum.Seven] = 7;
dict[MyEnum.Eight] = 8;
dict[MyEnum.Nine] = 9;
dict[MyEnum.Ten] = 10;
for (int i = 0; i < count; i++)
{
long sum = dict[MyEnum.Zero] +
dict[MyEnum.One] +
dict[MyEnum.Two] +
dict[MyEnum.Three] +
dict[MyEnum.Four] +
dict[MyEnum.Five] +
dict[MyEnum.Six] +
dict[MyEnum.Seven] +
dict[MyEnum.Eight] +
dict[MyEnum.Nine] +
dict[MyEnum.Ten];
}
}
[Benchmark]
public void Switch()
{
// dummy code to make benchmark more fair
Dictionary<MyEnum, int> dict = new Dictionary<MyEnum, int>();
dict[MyEnum.Zero] = 0;
dict[MyEnum.One] = 1;
dict[MyEnum.Two] = 2;
dict[MyEnum.Three] = 3;
dict[MyEnum.Four] = 4;
dict[MyEnum.Five] = 5;
dict[MyEnum.Six] = 6;
dict[MyEnum.Seven] = 7;
dict[MyEnum.Eight] = 8;
dict[MyEnum.Nine] = 9;
dict[MyEnum.Ten] = 10;
// end of dummy code
for (int i = 0; i < count; i++)
{
long sum = GetIntFromEnum(MyEnum.Zero) +
GetIntFromEnum(MyEnum.One) +
GetIntFromEnum(MyEnum.Two) +
GetIntFromEnum(MyEnum.Three) +
GetIntFromEnum(MyEnum.Four) +
GetIntFromEnum(MyEnum.Five) +
GetIntFromEnum(MyEnum.Six) +
GetIntFromEnum(MyEnum.Seven) +
GetIntFromEnum(MyEnum.Eight) +
GetIntFromEnum(MyEnum.Nine) +
GetIntFromEnum(MyEnum.Ten);
}
}
private int GetIntFromEnum(MyEnum fromMyEnum)
{
return fromMyEnum switch
{
MyEnum.Zero => 0,
MyEnum.One => 1,
MyEnum.Two => 2,
MyEnum.Three => 3,
MyEnum.Four => 4,
MyEnum.Five => 5,
MyEnum.Six => 6,
MyEnum.Seven => 7,
MyEnum.Eight => 8,
MyEnum.Nine => 9,
MyEnum.Ten => 10,
_ => throw new ArgumentOutOfRangeException(nameof(fromMyEnum), fromMyEnum, null)
};
}
[Benchmark]
public void String()
{
Dictionary<string, int> dict = new Dictionary<string, int>();
dict["Zero"] = 0;
dict["One"] = 1;
dict["Two"] = 2;
dict["Three"] = 3;
dict["Four"] = 4;
dict["Five"] = 5;
dict["Six"] = 6;
dict["Seven"] = 7;
dict["Eight"] = 8;
dict["Nine"] = 9;
dict["Ten"] = 10;
for (int i = 0; i < count; i++)
{
long sum = dict["Zero"] +
dict["One"] +
dict["Two"] +
dict["Three"] +
dict["Four"] +
dict["Five"] +
dict["Six"] +
dict["Seven"] +
dict["Eight"] +
dict["Nine"] +
dict["Ten"];
}
}
}
基准测试结果:
方法 | 意思 | 错误 | 标准偏差 |
---|---|---|---|
诠释 | 2.385 我们 | 0.0443 我们 | 0.0455 我们 |
枚举 | 2.502 我们 | 0.0415 我们 | 0.0388 我们 |
平等比较器 | 7.701 我们 | 0.0916 我们 | 0.0765 我们 |
转变 | 2.072 我们 | 0.0271 我们 | 0.0253 我们 |
细绳 | 6.765 我们 | 0.1316 我们 | 0.1293 我们 |
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.