什么让Enum.HasFlag这么慢?

我正在做一些速度testing,我注意到Enum.HasFlag比使用按位操作慢了大约16倍。

有没有人知道Enum.HasFlag的内部,为什么它如此缓慢? 我的意思是慢两倍的速度不会太糟糕,但是当它慢16倍时,这个function将无法使用。

如果有人想知道,这里是我用来testing其速度的代码。

using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace app { public class Program { [Flags] public enum Test { Flag1 = 1, Flag2 = 2, Flag3 = 4, Flag4 = 8 } static int num = 0; static Random rand; static void Main(string[] args) { int seed = (int)DateTime.UtcNow.Ticks; var st1 = new SpeedTest(delegate { Test t = Test.Flag1; t |= (Test)rand.Next(1, 9); if (t.HasFlag(Test.Flag4)) num++; }); var st2 = new SpeedTest(delegate { Test t = Test.Flag1; t |= (Test)rand.Next(1, 9); if (HasFlag(t , Test.Flag4)) num++; }); rand = new Random(seed); st1.Test(); rand = new Random(seed); st2.Test(); Console.WriteLine("Random to prevent optimizing out things {0}", num); Console.WriteLine("HasFlag: {0}ms {1}ms {2}ms", st1.Min, st1.Average, st1.Max); Console.WriteLine("Bitwise: {0}ms {1}ms {2}ms", st2.Min, st2.Average, st2.Max); Console.ReadLine(); } static bool HasFlag(Test flags, Test flag) { return (flags & flag) != 0; } } [DebuggerDisplay("Average = {Average}")] class SpeedTest { public int Iterations { get; set; } public int Times { get; set; } public List<Stopwatch> Watches { get; set; } public Action Function { get; set; } public long Min { get { return Watches.Min(s => s.ElapsedMilliseconds); } } public long Max { get { return Watches.Max(s => s.ElapsedMilliseconds); } } public double Average { get { return Watches.Average(s => s.ElapsedMilliseconds); } } public SpeedTest(Action func) { Times = 10; Iterations = 100000; Function = func; Watches = new List<Stopwatch>(); } public void Test() { Watches.Clear(); for (int i = 0; i < Times; i++) { var sw = Stopwatch.StartNew(); for (int o = 0; o < Iterations; o++) { Function(); } sw.Stop(); Watches.Add(sw); } } } } 

结果:HasFlag:52ms 53.6ms 55ms按位:3ms 3ms

有没有人知道Enum.HasFlag的内部,为什么它如此缓慢?

实际检查只是在Enum.HasFlag一个简单的位检查 – 这不是问题在这里。 这就是说,它比你自己的检查慢…

这种放缓有两个原因:

首先, Enum.HasFlag做了一个明确的检查,以确保枚举的types和标志的types都是相同的types,并从相同的枚举。 这项检查有一些成本。

其次,在HasFlag内部转换为UInt64过程中,存在一个不幸的方框和取值框。 我相信这是因为Enum.HasFlag需要所有的枚举,而不pipe底层的存储types如何。

这就是说, Enum.HasFlag有一个巨大的优势 – 它是可靠的,干净的,并使代码非常明显和expression。 在大多数情况下,我觉得这样做是值得的 – 但是如果你在一个非常性能的关键循环中使用它,可能值得做一下自己的检查。

反编译的Enum.HasFlags()代码如下所示:

 public bool HasFlag(Enum flag) { if (!base.GetType().IsEquivalentTo(flag.GetType())) { throw new ArgumentException(Environment.GetResourceString("Argument_EnumTypeDoesNotMatch", new object[] { flag.GetType(), base.GetType() })); } ulong num = ToUInt64(flag.GetValue()); return ((ToUInt64(this.GetValue()) & num) == num); } 

如果我猜测,我会说检查types是最慢的。

JITter应该把它作为一个简单的按位操作。 JITter足够了解甚至可以自定义处理某些框架方法(通过MethodImplOptions.InternalCall我认为?),但HasFlag似乎已经逃脱了微软的严重关注。

本页讨论的由于装箱造成的性能损失也影响公共.NET函数Enum.GetValuesEnum.GetNames ,它们分别向(Runtime)Type.GetEnumValues(Runtime)Type.GetEnumNames转发。

所有这些函数都使用一个(非generics的) Array作为返回types – 这对名称来说并不是那么糟糕(因为String是一个引用types),但是对于ulong[]值来说是相当不合适的。

这里有一个不良的代码(.NET 4.7):

 public override Array /* RuntimeType.*/ GetEnumValues() { if (!this.IsEnum) throw new ArgumentException(); ulong[] values = Enum.InternalGetValues(this); Array array = Array.UnsafeCreateInstance(this, values.Length); for (int i = 0; i < values.Length; i++) { var obj = Enum.ToObject(this, values[i]); // ew. boxing. array.SetValue(obj, i); // yuck } return array; // Array of object references, bleh. } 

我们可以看到,在执行副本之前, RuntimeType会再次返回到System.Enum以获取一个内部数组,即每个指定的Enum按需caching的单例。 还要注意, 这个 values数组的版本确实使用了正确的强签名ulong[]

这里是.NET函数(现在我们又回到了System.Enum )。 有一个类似的function获取名称(未显示)。

 internal static ulong[] InternalGetValues(RuntimeType enumType) => GetCachedValuesAndNames(enumType, false).Values; 

看到返回types? 这看起来像我们想要使用的函数…但是首先考虑一下.NET每次重新复制数组的第二个原因(如上面所见),.NET必须确保每个调用者都得到一个未更改的副本的原始数据,因为恶意编码者可能会改变其返回Array拷贝,引入持续的损坏。 因此,重新复制的预防措施特别旨在保护caching的内部主副本。

如果您不担心这种风险,也许是因为您确信自己不会意外地更改数组,或者只是为了避免几个周期(肯定是过早的)优化,可以很容易地获取内部caching数组任何Enum的名称或值的副本:

→以下两个function组成这篇文章的总和贡献←
→(但请参阅下面的编辑以改进版本)←

 static ulong[] GetEnumValues<T>() where T : struct => (ulong[])typeof(System.Enum) .GetMethod("InternalGetValues", BindingFlags.Static | BindingFlags.NonPublic) .Invoke(null, new[] { typeof(T) }); static String[] GetEnumNames<T>() where T : struct => (String[])typeof(System.Enum) .GetMethod("InternalGetNames", BindingFlags.Static | BindingFlags.NonPublic) .Invoke(null, new[] { typeof(T) }); 

注意T上的通用约束不足以保证Enum 。 为了简单起见,我不再检查struct之外的任何地方,但是你可能想要改进。 同样为了简单起见,这个(ref-fetches)每次都直接从MethodInforeflection出来,而不是试图构build和caching一个Delegate 。 原因是用非公开types的RuntimeType的第一个参数创build适当的委托是乏味的。 下面更多一点。

首先,我将结合使用示例:

 var values = GetEnumValues<DayOfWeek>(); var names = GetEnumNames<DayOfWeek>(); 

和debugging结果:

 'values' ulong[7] [0] 0 [1] 1 [2] 2 [3] 3 [4] 4 [5] 5 [6] 6 'names' string[7] [0] "Sunday" [1] "Monday" [2] "Tuesday" [3] "Wednesday" [4] "Thursday" [5] "Friday" [6] "Saturday" 

所以我提到Func<RuntimeType,ulong[]>的“第一个参数”很烦人。 然而,因为这个“问题”arg碰巧是第一个,所以有一个可爱的解决方法,你可以将每个特定的Enumtypes绑定为它自己的委托的Target ,然后每个types都被缩小到 Func<ulong[]> 。)

显而易见的是,使这些代表中的任何一个都没有意义,因为每个代表都是一个总是返回相同价值的函数……但是同样的逻辑似乎也适用于原来的情况(也就是说Func<RuntimeType,ulong[]> )。 虽然我们在这里只是一个代表,但是你永远不会真的想每个Enumtypes调用它一次以上 。 无论如何,所有这些导致了一个更好的解决scheme,这是包含在下面的编辑


[编辑:]
这是一个稍微更优雅的版本。 如果您将为相同的Enumtypes重复调用函数,则此处显示的版本将仅使用每个枚举types一次的reflection。 它将结果保存在本地可访问的caching中,以便随后进行非常快速的访问。

 static class enum_info_cache<T> where T : struct { static _enum_info_cache() { values = (ulong[])typeof(System.Enum) .GetMethod("InternalGetValues", BindingFlags.Static | BindingFlags.NonPublic) .Invoke(null, new[] { typeof(T) }); names = (String[])typeof(System.Enum) .GetMethod("InternalGetNames", BindingFlags.Static | BindingFlags.NonPublic) .Invoke(null, new[] { typeof(T) }); } public static readonly ulong[] values; public static readonly String[] names; }; 

这两个function变得微不足道:

 static ulong[] GetEnumValues<T>() where T : struct => enum_info_cache<T>.values; static String[] GetEnumNames<T>() where T : struct => enum_info_cache<T>.names; 

这里显示的代码展示了一种模式,它结合了三种特定的技巧,这些技巧似乎相互导致了一种不寻常的优雅的懒惰cachingscheme。 我发现特别的技术有惊人的广泛应用。

  1. 使用generics静态类来为每个不同的Enumcaching数组的独立副本。 值得注意的是,这是自动和按需要发生的。

  2. 与此相关, 加载器锁保证了独特的primefaces初始化,并且没有条件检查构造的混乱。 我们也可以只保留静态字段(由于显而易见的原因,它们通常不能用于其他惰性/延迟/请求方法)。

  3. 最后,我们可以利用C# types推断将generics函数 (入口点)自动映射到其各自的generics静态 ,以便需求caching最终被隐含驱动( ,最佳代码是不是那里 – 因为它永远不会有错误)

您可能已经注意到,这里显示的特定示例并没有很好地说明(3)点。 而不是依靠types推理, void -taking函数必须手动向前传播types参数T 我没有select公开这些简单的函数,以便有机会展示C#types推断如何使整体技术发光…

但是,你可以想象,当你结合一个可以推断出它的types参数的静态generics函数时 – 也就是说,你甚至不需要在调用站点提供它们 – 那么它就会变得非常强大。

关键的洞察是,虽然generics函数具有完整的types推断能力,但是generics不会,也就是说,如果您尝试调用以下第一行,编译器将永远不会推断T 但是我们仍然可以通过generics函数隐式types(最后一行)遍历到generics类,并且获得所有的好处。

 int t = 4; typed_cache<int>.MyTypedCachedFunc(t); // no inference from 't', explicit type required MyTypedCacheFunc<int>(t); // ok, (but redundant) MyTypedCacheFunc(t); // ok, full inference 

推荐的input可以毫不费力地将您引入适合自动需求caching的数据和行为,为每种types定制(回顾点1和2)。 如上所述,我觉得这个方法很有用,特别是考虑到它的简单性。