是什么使Enum.HasFlag如此缓慢?


69

我在进行一些速度测试时,发现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);
            }
        }
    }

}

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


6
因为枚举类型可以具有不同的基础类型。Enum.HasValue不能对此基本类型做任何假设,它必须假设最坏的情况。其中涉及使用UInt64和装箱的值。您的HashType函数是类型安全的。
汉斯·帕桑

1
您可能还希望看到以下内容:Why-enums-hasflag-method-need-boxing
13年

3
我刚刚使用.NET 4.6进行了基准测试:HasFlag: 8ms 8,7ms 11msBitwise: 4ms 4ms 4ms。因此,似乎他们已经改善了实施方式。
jeyk 2015年

在.NET Fiddle中使用4.7.2 HasFlag进行基准测试:8ms 9.4ms 16ms按位:5ms 5ms 5ms .NET Core 2.2甚至更糟:HasFlag:17ms 22.7ms 26ms按位:9ms 10.2ms 15ms
Bil Simser

Answers:


77

有谁知道Enum.HasFlag的内部原理,为什么它这么慢?

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

造成这种速度下降的原因有两个:

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

其次,有一个不幸的箱和值的拆箱一次转化的过程中UInt64发生的内部的HasFlag。我认为,这是由于要求Enum.HasFlag与所有枚举一起使用,而与基础存储类型无关。

话虽如此,它有一个巨大的优势Enum.HasFlag-它可靠,整洁,并使代码非常明显和富有表现力。在大多数情况下,我认为这值得付出成本-但如果您在性能非常关键的循环中使用它,则值得自己检查一下。


14
编写一个静态通用方法是可能的,但也不会太困难,该方法将采用两个相同类型的参数,并且如果该类型是从其派生的enum,则执行与Enum.HasFlag;相同的测试。这种方法的运行速度可以达到30倍Enum.HasFlag。稍微调整一下CIL,就可以开发出一种扩展方法,该方法将在IDE中使用从System.Enum其他类型派生但不与其他类型派生的类型弹出。我想知道为什么微软HasFlag不愿写东西,却没有使它表现出色吗?
2013年

6
这绝对是可怕的!我只是在数据集上分析了一个大型应用程序,该数据集创建了数百万个对象,并且执行了大量的算法处理,而枚举却很少。#1热门功能?Enum.HasFlag !! 我一直认为这相当于在Release版本中进行单个内联按位测试!如果我在MS负责这方面的工作,则必须等到问题解决后才能晚上入睡。
肯·贝克特

4
@KenBeckett我认为,部分原因是MS的C#==。NET重点-由于C#不允许对枚举进行通用约束,因此不能在没有装箱的情况下将其写在那里,这会造成问题。
Reed Copsey 2015年

1
Mono 4将a.HasFlag(b)whereab完全相同的类型转换为实际的按位与,并跳过方法中通常完成的所有重反射-或这样说
Şafak古尔

11
Enum.HasFlag现在已由.NET Core中的JIT优化
贾斯汀·范·帕滕

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

如果我猜到了,我会说检查类型是最让它慢下来的原因。

请注意,在最新版本的.Net Core中,此功能已得到改进,并可以Enum.HasFlag编译为与使用按位比较相同的代码。


8
我怀疑,尽管我不得不进行剖析,但ToUInt64(xxx.GetValue())实际上这是最糟糕的部分,因为它会执行盒子/取消盒子+ Convert.ToUInt64 ...
Reed Copsey

1
感谢您的代码片段。由于某些原因,.net反射器未显示.net 4程序集的代码。

8
@ReedCopsey-在.net 4.0中,实现方式有所不同,而是使用extern方法
BornToCode 2014年

4

本页讨论的由于装箱而导致的性能损失也影响公共.NET功能Enum.GetValuesEnum.GetNames,它们分别转发给(Runtime)Type.GetEnumValues(Runtime)Type.GetEnumNames

所有这些函数都使用(非泛型)Array作为返回类型,这对于名称来说并不算太糟糕(因为String是引用类型),但是对于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。还要注意,版本的values数组确实使用适当的强签名ulong[]

这是.NET函数(再次回到System.Enum现在)。有类似的功能来获取名称(未显示)。

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

看到返回类型吗?这看起来像是我们要使用的函数...但是,首先请考虑第二个原因。假设恶意编码者可以更改其返回的副本,从而Array造成持久性损坏。因此,重新复制预防措施尤其旨在保护缓存的内部主副本。

如果您不担心这种风险,也许是因为您确信不会意外更改阵列,或者只是想花一些循环(肯定过早的优化),则可以很容易地获取内部缓存的阵列任何名称或值的副本Enum

        →以下两个功能构成本文的总和←
        →(但请参阅下面的编辑以获取改进的版本)←

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,但您可能需要对此进行改进。同样为了简单起见,MethodInfo每次(ref-fetches和)都会直接反映出来,而不是尝试构建和缓存a Delegate。这样做的原因是,使用第一个非公共类型的参数创建适当的委托人RuntimeType是很乏味的。下面有更多内容。

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

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

和调试器结果:

'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恰好是第一个,所以有一个可爱的解决方法,您可以将每种特定Enum类型绑定为Target自己的委托,然后 每种简化为Func<ulong[]>。)

显然,它的无意义做任何的那些代表,因为每个也只是一个函数总是返回相同的值...但同样的逻辑似乎适用,可能不太明显,到原来的状况,以及(即Func<RuntimeType,ulong[]>)。尽管我们在这里只接待了一个委托,但对于每个Enum类型,您永远都不会真正想多次调用它。无论如何,所有这些都会导致更好的解决方案,该解决方案包含在下面的编辑中。


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

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

这两个功能变得微不足道:

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

此处显示的代码说明了组合三个特定技巧的模式,这些技巧似乎相互导致异常的优雅的惰性缓存方案。我发现特定技术具有令人惊讶的广泛应用。

  1. 使用通用静态类为每个不同的缓存数组的独立副本Enum。值得注意的是,这是自动发生并按需进行的。

  2. 与此相关的是,加载程序锁可确保唯一的原子初始化,并且不会造成条件检查构造的混乱。我们还可以使用来保护静态字段readonly(出于显而易见的原因,通常不能将其与其他惰性/延迟/需求方法一起使用);

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

您可能已经注意到,此处显示的特定示例并未很好地说明第(3)点。void-take函数必须依赖于类型推断,而不是依赖于类型推断T。我没有选择公开这些简单的函数,因此没有机会展示C#类型推理如何使整体技术大放异彩...

但是,你能想象,当你结合静态的通用功能,可以推断出它的类型参数(S) -即,所以你甚至不必在调用站点提供他们-那么它得到相当强大。

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

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

精心设计的推断类型可以毫不费力地将您带入针对每种类型(调用点1和2)定制的相应的自动需求缓存数据和行为。如前所述,我发现该方法很有用,尤其是考虑到其简单性。


我想我在尝试理解所有内容时都伤了脑筋...并且由于必须使用字典,学会了一些新的英语单词。h!但基本上,枚举可归结为这一点:在速度至关重要的情况下,不要使用HasFlag(或其他类似方法)-进行位操作吗?
Scre

3

JITter应该将其内联为一个简单的按位操作。JITter知道足以自定义处理某些框架方法(我想通过MethodImplOptions.InternalCall吗?),但是HasFlag似乎已经摆脱了Microsoft的严重关注。


1
函数的编写方式,JITter无法对其进行优化。如果Enum1Enum2枚举类型,代码Enum1.HasFlag(Enum2)需要创建持有举行由值的新堆对象实例Enum1Enum2,然后通过这些对象的一些例程是太大了进行分析。JITter实际上别无选择,只能生成创建那些堆对象的代码,而这些对象的创建完全影响了性能。
2013年

如果JITter知道Enum1和Enum2具有相同的基础类型,则可以放弃box指令并内联该特定调用。没有理由每次都必须进行装箱操作。
Joshua A. Schaeffer

2
没有通用HasFlag方法,就无法避免装箱。问题是,如果Enum1Enum2是相同的具体枚举类型,但是Enum3System.EnumEnum1.HasFlag(Enum2)必须调用与相同的JITted代码Enum1.HasFlag(Enum3)。如果非泛型方法重载可以接受对装箱值类型的堆引用,则重载除了接受堆引用之外没有其他方法,也无法将值类型隐式转换为的堆引用。兼容类型,但通过拳击除外。
2013年

如果JITter不知道,则编译器应该。一个或另一个具有优化Enum1.HasFlag(Enum2)特殊情况所需的信息。当实际使用拳击时,可以单独构建拳击操作。
Joshua A. Schaeffer 2014年

我希望不要只是拥有一个,等System.Enum的家族System.Int32Enum,而System.Int64Enum每个家族都有一个合适的Value成员,或者希望类型系统允许从值类型中派生类型,前提是派生类型不添加任何值。新的字段(在这种情况下enum类型的可从派生Int32Int64等根据需要)。但实际上,所生成的代码enum1.HasFlag(enum2)需要根据所讨论的类型而有所不同,并且没有任何一种方法都无法做到这一点……
supercat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.