【问题标题】:What is it that makes Enum.HasFlag so slow?是什么让 Enum.HasFlag 这么慢?
【发布时间】:2011-11-14 04:22:13
【问题描述】:

我正在做一些速度测试,我注意到 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
Bitwise: 3ms 3ms 3ms

【问题讨论】:

  • 因为枚举类型可以有不同的底层基类型。 Enum.HasValue 不能对该基本类型做出任何假设,它必须假设最坏的情况。这涉及使用 UInt64 和盒装值。您的 HashType 函数是类型安全的。
  • 你可能还想看看这个:why-enums-hasflag-method-need-boxing
  • 我刚刚使用 .NET 4.6 对此进行了基准测试:HasFlag: 8ms 8,7ms 11msBitwise: 4ms 4ms 4ms。所以他们似乎改进了实施。
  • 在 .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跨度>

标签: c# .net


【解决方案1】:

有谁知道 Enum.HasFlag 的内部结构以及它为什么这么慢?

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

这种放缓有几个原因:

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

其次,在转换为UInt64 的过程中,在HasFlag 内部出现了一个不幸的值的框和拆箱。我相信这是因为Enum.HasFlag 需要与所有枚举一起工作,而不管底层存储类型如何。

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

【讨论】:

  • 编写一个静态泛型方法是可能的,并且不会太难,该方法将接受两个相同类型的参数,如果该类型派生自enum,则执行与Enum.HasFlag相同的测试;这种方法的运行速度可以达到Enum.HasFlag 的 30 倍。通过一点 CIL 调整,可以创建一个扩展方法,该方法会在 IDE 中弹出,其类型派生自 System.Enum,但不会使用其他类型。我想知道为什么微软费心写HasFlag,却连远程性能都没有?
  • 这绝对是可怕的!我刚刚在一个数据集上分析了一个大型应用程序,它创建了数百万个对象并进行了大量算法处理,而对枚举几乎没有。 #1 热门功能?枚举.HasFlag!!我一直认为这相当于发布版本中的单个内联按位测试!如果我在 MS 负责这方面的工作,我将无法在晚上睡觉,直到这个问题得到解决。
  • @KenBeckett 我认为,部分原因是 MS 对 C#==.NET 的巨大关注——因为 C# 不允许对枚举进行通用约束,所以如果没有装箱,你就不能在那里写这个,这会导致有问题。
  • Mono 4 转换 a.HasFlag(b) 其中 ab 是完全相同的类型,实际按位与并跳过通常在方法中完成的所有重反射 - or so they say
  • Enum.HasFlag 现在是 .NET Core 中的 optimized by the JIT
【解决方案2】:

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 编译为与使用按位比较相同的代码。

【讨论】:

  • 我怀疑,虽然我不得不描述一下,ToUInt64(xxx.GetValue()) 实际上是最糟糕的部分,因为它确实是一个 box/unbox + Convert.ToUInt64...
  • 感谢代码 sn-p。由于某种原因,.net 反射器没有显示 .net 4 程序集的代码。
  • @ReedCopsey - 在 .net 4.0 中,实现是不同的,而是使用外部方法
【解决方案3】:

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

所有这些函数都使用(非泛型)Array 作为返回类型——这对于名称来说还不错(因为 String 是一个引用类型)——但对于 @ 987654331@ 值。

下面是有问题的代码 (.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 缓存的单例。另请注意,版本的值数组确实使用了正确的强签名ulong[]

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

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

看到返回类型了吗?这看起来像是我们想要使用的函数...但首先考虑 .NET 每次重新复制数组的第二个原因(如您在上面看到的)是 .NET 必须确保每个调用者都获得未更改的副本原始数据,因为恶意编码员可以更改她返回的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 上,而不是尝试构建和缓存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&lt;RuntimeType,ulong[]&gt; 的“第一个论点”很烦人。但是,因为这个“问题”arg 恰好是第一个,所以有一个可爱的解决方法,您可以将每个特定的 Enum 类型绑定为其自己的委托的 Target,然后每个都是 reduced to Func&lt;ulong[]&gt;。)

显然,制作任何 那些 委托都是没有意义的,因为每个委托都只是一个总是返回相同值的函数......但同样的逻辑似乎适用于,也许不太明显,原来的情况也是如此(即Func&lt;RuntimeType,ulong[]&gt;)。尽管我们这里只有一个委托,但您永远不会真的想多次调用它每个枚举类型。无论如何,所有这一切都导致了一个更好的解决方案,它包含在下面的编辑中。


[编辑:]
这是同一事物的稍微更优雅的版本。如果您要为相同的 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. 与此相关,loader lock 保证了唯一的原子初始化,并且没有条件检查结构的混乱。我们还可以使用readonly 保护静态字段(出于显而易见的原因,它通常不能与其他惰性/延迟/需求方法一起使用);

  3. 最后,我们可以利用 C# type inference 自动将通用函数(入口点)映射到其各自的通用静态中,这样需求缓存最终甚至被隐式驱动(,最好的代码是不存在的代码——因为它永远不会有错误)

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

但是,您可以想象,当您组合一个可以推断其类型参数的静态泛型函数时——即,您甚至不必在调用时提供它们网站——然后它变得非常强大。

关键的见解是,虽然泛型函数具有完整的类型推断能力,但泛型没有,也就是说,编译器永远不会推断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)。如前所述,我发现这种方法很有用,尤其是考虑到它的简单性。

【讨论】:

  • 我想我在试图理解这一切时打破了我的大脑......并且由于不得不使用字典而学习了一些新的英语单词。呵!但基本上枚举的东西归结为:当速度很关键时不要使用 HasFlag(或任何类似的东西) - 去 bit-ops?
【解决方案4】:

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

【讨论】:

  • 函数的写法,JITter无法优化。如果Enum1Enum2 是枚举类型,则需要代码Enum1.HasFlag(Enum2) 来创建新的堆对象实例,这些实例保存Enum1Enum2 保存的值,然后将这些对象传递给一些例程太大了,无法分析。 JITter 真的别无选择,只能生成创建这些堆对象的代码,而这些对象的创建完全是狗的表现。
  • 如果 JITter 知道 Enum1 和 Enum2 具有相同的底层类型,它可以放弃 box 指令并内联该特定调用。没有理由每次都必须进行装箱操作。
  • 如果没有通用的HasFlag 方法,就无法避免装箱。问题在于,如果Enum1Enum2 是相同的具体枚举类型,但Enum3System.Enum,则Enum1.HasFlag(Enum2) 必须调用与Enum1.HasFlag(Enum3) 相同的JITted 代码。如果非泛型方法重载可以接受对装箱值类型的堆引用,则重载无法接受除堆引用之外的任何内容,也无法将值类型隐式转换为堆引用一种兼容的类型,除了通过拳击。
  • 如果 JITter 不知道,那么编译器应该知道。一个或另一个具有优化 Enum1.HasFlag(Enum2) 特殊情况所需的信息。装箱操作可以在实际使用装箱时单独构建。
  • 我希望不是只有一个System.Enum,而是有一个System.Int32EnumSystem.Int64Enum等家族,每个家族都有一个合适的Value成员,否则类型系统允许从值类型派生类型,前提是派生类型没有添加任何新字段(在这种情况下,enum 类型可以根据需要从 Int32Int64 等派生)。尽管如此,enum1.HasFlag(enum2) 的生成代码需要根据所讨论的类型而有所不同,没有任何一种方法都无法做到这一点......
猜你喜欢
  • 2019-11-07
  • 2013-08-21
  • 2012-05-01
  • 2023-03-29
  • 1970-01-01
  • 1970-01-01
  • 2020-05-14
  • 2011-05-21
  • 2021-09-03
相关资源
最近更新 更多