繁体   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