字典枚举关键性能

时间:2014-10-09 14:17:30

标签: c# performance dictionary enums mono

我对使用密钥枚举的通用词典感到担忧。

如下页所述,使用键的枚举将分配内存: 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>的性能?

任何建议都非常感谢。

3 个答案:

答案 0 :(得分:31)

问题是拳击。它是将值类型转换为对象的行为,可能或可能不是必需的。

Dictionary比较密钥的方式,基本上是,它将使用EqualComparer<T>.Default,并调用GetHashCode()来查找正确的存储区,并Equals进行比较是否存在&#39 ; s桶中任何与我们正在寻找的值相等的值。

好处是:.NET框架具有良好的优化,在"Enum integers"的情况下避免装箱。见CreateComparer()。你不太可能在这里看到整数和枚举之间的任何差异,作为关键。

这里要注意:这不是一件容易的行为,事实上,如果你深入挖掘,你会得出结论,这场战斗的四分之一是通过CLR&#34; hacks&#34;来实现的。如下所示:

   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的getILIntrinsicImplementation究竟发生了什么?我也想知道。在这个正确的时刻不确定如何检查它。它在运行时用特定的IL代码替换了吗?我相信?!

MONO

现在,回答你的问题:是的,你是对的。 Enum作为Mono的关键,在紧密循环中会变慢。这是因为Mono在Enums上做拳击,就像我所看到的那样。您可以查看EnumIntEqualityComparer,正如您所看到的,它通过装箱Array.UnsafeMov调用T基本上将(int)(object) instance;类型转换为整数。这是&#34; classic&#34;泛型的限制,并没有很好的解决方案来解决这个问题。

解决方案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比较器,并使用一些避免拆箱的技巧。我在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);
    }
}

解决方案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上给你更好的表现。

答案 1 :(得分:1)

我在一段时间后遇到了同样的问题并最终将它合并到我编写的通用枚举扩展和辅助方法的库中(它是用C ++ / CLI(编译的AnyCPU)编写的,因为C#不允许创建类型约束对于枚举类型)。它可以在NuGetGitHub

的Apache 2.0许可下使用

您可以通过从库中的静态Dictionary类型中抓取IEqualityComparer,在Enums中实现它:

var equalityComparer = Enums.EqualityComparer<MyEnum>();
var dictionary = new Dictionary<MyEnum, MyValueType>(equalityComparer);

使用类似于已经提供的一个答案中提到的UnsafeEnumCast的技术,在没有装箱的情况下处理这些值(因为它是不安全,因此在测试中被覆盖)。因此,它非常快(因为在这种情况下,这将是替换相等比较器的唯一点)。包含基准测试应用程序以及从构建PC生成的最新结果。

答案 2 :(得分:1)

作为字典键的枚举现在具有与 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 毫秒左右,并且在大多数运行中枚举版本稍快。