简体   繁体   English

字典枚举关键性能

[英]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 代码?!

MONO单核细胞增多症

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.这是泛型的“经典”限制,这个问题没有很好的解决方案。

Solution 1解决方案1

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编写它会让人觉得很愚蠢。

Solution 2解决方案2

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);
    }
}

Solution 3解决方案3

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它在NuGetGitHub上的 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 2.385 我们 0.0443 us 0.0443 我们 0.0455 us 0.0455 我们
Enum枚举 2.502 us 2.502 我们 0.0415 us 0.0415 我们 0.0388 us 0.0388 我们
EqualityComparer平等比较器 7.701 us 7.701 我们 0.0916 us 0.0916 我们 0.0765 us 0.0765 我们
Switch转变 2.072 us 2.072 我们 0.0271 us 0.0271 我们 0.0253 us 0.0253 我们
String细绳 6.765 us 6.765 我们 0.1316 us 0.1316 我们 0.1293 us 0.1293 我们

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM