[英]dictionary enum key performance
I have a concern about generic dictionaries using enums for keys.我担心使用枚举作为键的通用词典。
As stated at the below page, using enums for keys will allocate memory: http://blogs.msdn.com/b/shawnhar/archive/2007/07/02/twin-paths-to-garbage-collector-nirvana.aspx如下页所述,对键使用枚举将分配内存:http: //blogs.msdn.com/b/shawnhar/archive/2007/07/02/twin-paths-to-garbage-collector-nirvana.aspx
I've tested and confirmed the behavior, and it's causing problems in my project.我已经测试并确认了该行为,它在我的项目中引起了问题。 For readability, I believe using enums for keys is very useful, and the optimal solution for me would be to write write a class implementing
IDictionary<TKey, TValue>
, which would use integers for keys internally.为了可读性,我相信对键使用枚举非常有用,对我来说最佳解决方案是编写一个实现
IDictionary<TKey, TValue>
的类,它将在内部使用整数作为键。 The reason is I don't want to change all my existing dictionaries to use integers for keys, and do implicit casting.原因是我不想更改所有现有词典以使用整数作为键,并进行隐式转换。 This would be best performance wise, but it will give me lot of work initially and it will reduce the readability.
这将是最好的性能明智,但它会给我很多工作,并且会降低可读性。
So I've tried a couple of approaches, including using GetHashCode
(which unfortunately allocates memory) to build an internal Dictionary<int, TValue>
.所以我尝试了几种方法,包括使用
GetHashCode
(不幸的是分配内存)来构建内部Dictionary<int, TValue>
。
So, to wrap it up in one question;因此,将其总结为一个问题; can anyone think of a solution that I can use to keep the readability of
Dictionary<SomeEnum, TValue>
, while having the perfomance of a Dictionary<int, TValue>
?谁能想到一个解决方案,我可以用它来保持
Dictionary<SomeEnum, TValue>
的可读性,同时具有Dictionary<int, TValue>
的性能?
Any advice much appreciated.非常感谢任何建议。
The problem is boxing .问题是拳击。 It's an act of turning value type into object, which might, or might not be unnecessary.
这是将值类型转换为对象的行为,这可能是不必要的,也可能不是。
The way Dictionary
compares keys, is essentially, that it will use EqualComparer<T>.Default
, and call GetHashCode()
to find correct bucket, and Equals
to compare if there's any value in the bucket that is equal tot he one we're looking for. Dictionary
比较键的方式本质上是,它将使用EqualComparer<T>.Default
,并调用GetHashCode()
来查找正确的存储桶,并使用Equals
来比较存储桶中是否有任何值与我们的值相等寻找。
The good thing is this: .NET framework has good optimizations, which avoid boxing in the case of "Enum integers"
.好消息是:.NET 框架有很好的优化,可以避免在
"Enum integers"
的情况下装箱。 See CreateComparer() .请参阅CreateComparer() 。 It's highly unlikely that you will see any difference here, between integers and enums, as keys.
您在这里看到整数和枚举作为键的任何区别的可能性很小。
To note here: this is not an easy act, in fact, if you dig in deep, you'll come to conclusion that quarter of this battle is implemented through CLR "hacks".这里要注意:这不是一件容易的事,事实上,如果你深入挖掘,你会得出结论,这场战斗的四分之一是通过 CLR“黑客”实现的。 As seen here:
如这里所见:
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();
}
It could be definitely easier if generics had Enum constraint, and perhaps even something a long of the lines UnsafeEnumCast<T>(T val) where T : Enum->Integer
, but well... they don't.如果泛型有 Enum 约束,甚至可能有很长的行
UnsafeEnumCast<T>(T val) where T : Enum->Integer
,那肯定会更容易,但好吧......他们没有。
You might be wondering, what exactly is going on in getILIntrinsicImplementation for that EnumCast
?您可能想知道,该
EnumCast
什么? I wonder too.我也想知道Not exactly sure as of this right moment how to check it.
不完全确定在这个正确的时刻如何检查它。 It's replaced on run-time with specific IL code I believe?!
我相信它在运行时被替换为特定的 IL 代码?!
Now, answer to your question: yes you're right.现在,回答你的问题:是的,你是对的。
Enum
as a key on Mono, will be slower in a tight loop. Enum
作为 Mono 上的键,在紧密循环中会变慢。 It's because Mono does boxing on Enums, as far I can see.据我所知,这是因为 Mono 在枚举上进行拳击。 You can check out EnumIntEqualityComparer , as you can see, it calls
Array.UnsafeMov
that basically casts a type of T
into integer, through boxing: (int)(object) instance;
您可以查看EnumIntEqualityComparer ,如您所见,它调用
Array.UnsafeMov
基本上将T
类型转换为整数,通过装箱: (int)(object) instance;
. . That's the "classical" limitation of generics, and there is no nice solution for this problem.
这是泛型的“经典”限制,这个问题没有很好的解决方案。
Implement an EqualityComparer<MyEnum>
for your concrete Enum.为您的具体枚举实现
EqualityComparer<MyEnum>
。 This will avoid all the casting.这将避免所有的铸造。
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;
}
}
All you need to do then, is pass it to your Dictionary
:然后,您需要做的就是将其传递给您的
Dictionary
:
new Dictionary<MyEnum, int>(new MyEnumComparer());
It works, it gives you the same performance as it is with integers, and avoids boxing issues.它有效,它为您提供与整数相同的性能,并避免装箱问题。 The problem is though, this is not generic and writing this for each
Enum
can feel stupid.问题是,这不是通用的,为每个
Enum
编写它会让人觉得很愚蠢。
Writing a generic Enum
comparer, and using few tricks that avoids unboxing.编写一个通用的
Enum
比较器,并使用一些避免拆箱的技巧。 I wrote this with a little help from here ,我从这里得到了一点帮助,写了这个,
// 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);
}
}
Now, there's a little problem with the solution#2, as Expression.Compile()
is not that famous on iOS(no runtime code generation), and some mono versions don't have ??现在,解决方案#2 有一个小问题,因为
Expression.Compile()
在 iOS 上不是那么出名(没有运行时代码生成),而且一些单声道版本没有 ?? Expression.Compile
?? Expression.Compile
编译 ?? (not sure). (没有把握)。
You can write simple IL code that will take care of the enum conversion, and compile it.您可以编写简单的 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
}
}
In order to compile it into an assembly, you have to call:为了将其编译为程序集,您必须调用:
ilasm enum2int.il /dll
where enum2int.il is the text file containing IL. ilasm enum2int.il /dll
其中 enum2int.il 是包含 IL 的文本文件。
You can now reference the given assembly( enum2int.dll
) and call the static method, as such:您现在可以引用给定的程序集(
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);
}
}
It might seem to be killer code, but it avoids boxing, and it should give you better berformance on Mono
.它可能看起来是杀手级代码,但它避免了装箱,并且应该在
Mono
上为您提供更好的性能。
I ran into this same problem a while back and ended up incorporating it into a library I wrote of generic enum extension and helper methods (it's written in C++/CLI (compiled AnyCPU) because C# doesn't allow creation of type constraints for enum types).不久前我遇到了同样的问题,最终将它合并到我编写的通用枚举扩展和辅助方法的库中(它是用 C++/CLI(编译的 AnyCPU)编写的,因为 C# 不允许为枚举类型创建类型约束) )。 It's available under the Apache 2.0 license on NuGet and GitHub
它在NuGet和GitHub上的 Apache 2.0 许可下可用
You can implement it in a Dictionary
by grabbing the IEqualityComparer
from the static Enums
type in the library:您可以通过从库中的静态
Enums
类型中获取IEqualityComparer
来在Dictionary
实现它:
var equalityComparer = Enums.EqualityComparer<MyEnum>();
var dictionary = new Dictionary<MyEnum, MyValueType>(equalityComparer);
The values are handled without boxing, using a technique similar to the UnsafeEnumCast
mentioned in one of the answers already provided (covered to death in tests since it is unsafe ).这些值是在没有装箱的情况下处理的,使用类似于已经提供的一个答案中提到的
UnsafeEnumCast
的技术(在测试中被覆盖,因为它是不安全的)。 As a result, it's very fast (since that would be the only point of replacing an equality comparer in this case).结果,它非常快(因为在这种情况下这将是替换相等比较器的唯一要点)。 A benchmarking app is included as well as recent results generated from my build PC.
包括一个基准测试应用程序以及我构建的 PC 生成的最新结果。
Enums as dictionary keys now have the same or better performance as int
dictionary keys.作为字典键的枚举现在具有与
int
字典键相同或更好的性能。 I measured this using NUnit:我使用 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);
}
}
The time taken by these two tests on my Ryzen 5 PC in a .NET 5.0 Release build is consistently around 300ms, and the enum version is slightly faster on most runs.在我的 Ryzen 5 PC 上的 .NET 5.0 Release 版本中,这两个测试所花费的时间始终在 300 毫秒左右,并且在大多数运行中,枚举版本稍微快一些。
In later .net versions (tested for .NET7), performance is the same as using int
as key.在以后的 .net 版本中(针对 .NET7 测试),性能与使用
int
作为键相同。 Using an IEqualityComparer
struct as the Dictionary's constructor argument even make performance worse.使用
IEqualityComparer
结构作为 Dictionary 的构造函数参数甚至会使性能变差。 For reference, here is some code that shows some alternatives and corresponding performances.作为参考,这里有一些代码显示了一些替代方案和相应的性能。 The code uses BenchmarkDotNet framwork.
该代码使用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"];
}
}
}
Benchmarking results :基准测试结果:
Method![]() |
Mean![]() |
Error![]() |
StdDev![]() |
---|---|---|---|
Int![]() |
2.385 us ![]() |
0.0443 us ![]() |
0.0455 us ![]() |
Enum![]() |
2.502 us ![]() |
0.0415 us ![]() |
0.0388 us ![]() |
EqualityComparer![]() |
7.701 us ![]() |
0.0916 us ![]() |
0.0765 us ![]() |
Switch![]() |
2.072 us ![]() |
0.0271 us ![]() |
0.0253 us ![]() |
String![]() |
6.765 us ![]() |
0.1316 us ![]() |
0.1293 us ![]() |
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.