是什么让Enum.HasFlag这么慢?

Wil*_*ill 64 .net c#

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

有谁知道Enum.HasFlag的内部以及为什么它如此之慢?我的意思是两倍慢不会太糟糕但是当它慢了16倍时它会使该功能无法使用.

如果有人想知道,这是我用来测试其速度的代码.

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

}
Run Code Online (Sandbox Code Playgroud)

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

Ree*_*sey 74

有谁知道Enum.HasFlag的内部以及为什么它如此之慢?

实际检查只是一个简单的检查Enum.HasFlag- 这不是问题.话虽如此,它比你自己的位检查要慢......

这种放缓有几个原因:

首先,Enum.HasFlag进行显式检查以确保枚举的类型和标志的类型都是相同的类型,并且来自相同的枚举.这张支票有一些费用.

其次,在转换到UInt64内部时,有一个不幸的框和unbox值HasFlag.我相信这是因为要求Enum.HasFlag所有枚举都能使用,无论底层存储类型如何.

话虽如此,它有一个巨大的优势Enum.HasFlag- 它可靠,干净,并使代码非常明显和富有表现力.在大多数情况下,我认为这使得它值得花费 - 但如果你在一个性能非常关键的循环中使用它,那么值得你自己检查一下.

  • 编写一个静态泛型方法是可能的,而不是太困难,它将采用相同类型的两个参数,如果该类型派生自`enum`,则执行与`Enum.HasFlag`相同的测试; 这样的方法可以以'Enum.HasFlag`的速度运行30倍.通过一点CIL调整,可以创建一个扩展方法,它将在IDE中弹出,类型派生自`System.Enum`但不包含其他类型.我想知道为什么微软打算写'HasFlag`,但是它甚至不能使它具有远程性能? (14认同)
  • `Enum.HasFlag`现在由.NET中的[JIT]优化(https://github.com/dotnet/coreclr/pull/13748). (9认同)
  • 这绝对是非常可怕的!我只是在数据集上分析了一个大型应用程序,该数据集创建了数百万个对象,并进行了大量的算法处理,这些处理对枚举很少.#1热门功能?Enum.HasFlag!我一直认为这相当于Release版本中的单个内联按位测试!如果我在MS的这个区域负责,那么在修复之前我将无法入睡. (5认同)
  • @KenBeckett部分地,我认为,MS的巨大C#==.NET焦点 - 因为C#不允许枚举的通用约束,你不能在没有装箱的情况下写这个,这导致这是有问题的. (4认同)

svi*_*ick 26

解密的代码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);
}
Run Code Online (Sandbox Code Playgroud)

如果我猜,我会说检查类型是最慢的减速.

  • @ReedCopsey - 在.net 4.0中,实现方式不同,而是使用extern方法 (8认同)
  • 我怀疑,虽然我不得不说,`ToUInt64(xxx.GetValue())`实际上是最糟糕的部分,因为它有一个box/unbox + Convert.ToUInt64 ...... (7认同)

Gle*_*den 5

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

\n\n

所有这些函数都使用(非泛型)Array作为返回类型——这对于名称来说还不错(因为String是引用类型)——但对于ulong[]值来说却非常不合适。

\n\n

下面是有问题的代码 (.NET 4.7):

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

我们可以看到,在进行复制之前,RuntimeType再次返回以System.Enum获取内部数组,这是一个根据需要为每个特定Enum. 另请注意,版本的值数组确实使用了正确的强签名ulong[].

\n\n

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

\n\n
internal static ulong[] InternalGetValues(RuntimeType enumType) => \n    GetCachedValuesAndNames(enumType, false).Values;\n
Run Code Online (Sandbox Code Playgroud)\n\n

看到返回类型了吗?这看起来像是我们想要使用的函数...但首先考虑 .NET 每次重新复制数组的第二个原因(如您在上面看到的)是 .NET 必须确保每个调用者获得一个未更改的数组原始数据的副本,因为恶意编码人员可能会更改返回的副本Array,从而导致持久损坏。因此,重新复制预防措施特别旨在保护缓存的内部主副本。

\n\n

如果您不担心这种风险,也许是因为您确信自己不会意外更改数组,或者可能只是为了维持几个周期的(这肯定为时过早)优化,那么它就可以了。获取任何名称或值的内部缓存数组副本很简单Enum

\n\n

        \xe2\x86\x92 以下两个函数构成了本文的贡献之和 \xe2\x86\x90
\n        \xe2\x86\x92 (但请参阅下面的编辑以获得改进版本) \xe2\x86\x90

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

请注意,通用约束T并不足以保证Enum. 为简单起见,我不再检查其他内容struct,但您可能想对此进行改进。另外为了简单起见,这(引用和)MethodInfo每次都会直接反映,而不是尝试构建和缓存Delegate. 原因是使用非公共类型的第一个参数创建正确的委托RuntimeType是很乏味的。下面对此进行更多介绍。

\n\n

首先,我将用用法示例来总结:

\n\n
var values = GetEnumValues<DayOfWeek>();\nvar names = GetEnumNames<DayOfWeek>();\n
Run Code Online (Sandbox Code Playgroud)\n\n

和调试器结果:

\n\n
\'values\'    ulong[7]\n[0] 0\n[1] 1\n[2] 2\n[3] 3\n[4] 4\n[5] 5\n[6] 6\n\n\'names\' string[7]\n[0] "Sunday"\n[1] "Monday"\n[2] "Tuesday"\n[3] "Wednesday"\n[4] "Thursday"\n[5] "Friday"\n[6] "Saturday"\n
Run Code Online (Sandbox Code Playgroud)\n\n

所以我提到“第一个论点”Func<RuntimeType,ulong[]>反思起来很烦人。然而,因为这个“问题”arg 恰好是第一个,所以有一个可爱的解决方法,您可以将每个特定Enum类型绑定为Target它自己的委托,然后每个类型都减少为 Func<ulong[]>。)

\n\n

显然,创建任何这些委托都是没有意义的,因为每个委托只是一个始终返回相同值的函数......但相同的逻辑似乎也适用于原始情况(即,Func<RuntimeType,ulong[]>),也许不太明显。尽管我们在这里确实只使用了一个委托,但您永远不会真正想对每个 Enum 类型调用它多次。无论如何,所有这些都会带来更好的解决方案,该解决方案包含在下面的编辑中。

\n\n
\n\n

[编辑:]
这是同一件事的一个稍微更优雅的版本。如果您要为同一类型重复调用函数Enum,则此处显示的版本将仅对每个 Enum 类型使用反射一次。它将结果保存在本地可访问的缓存中,以便随后极快速地访问。

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

这两个函数变得微不足道:

\n\n
static ulong[] GetEnumValues<T>() where T : struct => enum_info_cache<T>.values;\nstatic String[] GetEnumNames<T>() where T : struct => enum_info_cache<T>.names;\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里显示的代码说明了一种组合三个特定技巧的模式,这三个技巧似乎共同产生了一种异常优雅的惰性缓存方案。我发现这种特殊的技术有着令人惊讶的广泛应用。

\n\n
    \n
  1. 使用通用静态类来缓存每个不同的数组的独立副本Enum。值得注意的是,这是根据需要自动发生的;

  2. \n
  3. 与此相关的是,加载器锁保证了唯一的原子初始化,并且在没有混乱的条件检查结构的情况下实现了这一点。我们还可以保护静态字段readonly(由于显而易见的原因,通常不能与其他惰性/延迟/需求方法一起使用);

  4. \n
  5. 最后,我们可以利用 C#类型推断来自动将通用函数(入口点)映射到其各自的通用静态类中,以便最终甚至隐式驱动需求缓存(,最好的代码是不存在的代码)那里——因为它永远不会有错误)

  6. \n
\n\n

您可能注意到这里显示的特定示例并没有很好地说明第 (3) 点。void-takeing 函数必须手动向前传播类型参数,而不是依赖于类型推断T。我没有选择公开这些简单的函数,这样就有机会展示 C# 类型推断如何使整体技术大放异彩...

\n\n

但是,您可以想象,当您确实组合了一个可以推断其类型参数的静态泛型函数时(即,您甚至不必在调用站点提供它们),那么它就会变得非常强大。

\n\n

关键的见解是,虽然泛型函数具有完整的类型推断功能,但泛型却没有,也就是说,T如果您尝试调用以下行中的第一行,编译器将永远不会推断。但我们仍然可以通过泛型函数隐式类型(最后一行)遍历泛型类,从而获得对泛型类的完全推断访问,以及由此带来的所有好处:

\n\n
int t = 4;\ntyped_cache<int>.MyTypedCachedFunc(t);  // no inference from \'t\', explicit type required\n\nMyTypedCacheFunc<int>(t);               // ok, (but redundant)\n\nMyTypedCacheFunc(t);                    // ok, full inference\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果设计得当,推断类型可以毫不费力地让您进入适当的自动按需缓存的数据和行为,并为每种类型进行定制(回顾第 1 点和第 2 点)。如前所述,我发现该方法很有用,特别是考虑到它的简单性。

\n