簡體   English   中英

字典枚舉關鍵性能

[英]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; . 這是泛型的“經典”限制,這個問題沒有很好的解決方案。

解決方案1

為您的具體枚舉實現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編寫它會讓人覺得很愚蠢。

解決方案2

編寫一個通用的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);
    }
}

解決方案3

現在,解決方案#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# 不允許為枚舉類型創建類型約束) )。 它在NuGetGitHub上的 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM