[英]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.