簡體   English   中英

是什么讓 Enum.HasFlag 如此緩慢?

[英]What is it that makes Enum.HasFlag so slow?

我正在做一些速度測試,我注意到 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.HasFlag 的內部結構以及為什么它這么慢?

實際檢查只是Enum.HasFlag一個簡單位檢查 - 這不是這里的問題。 話雖如此,它比您自己的位檢查慢...

這種放緩有幾個原因:

首先, Enum.HasFlag做一個顯式檢查以確保枚舉的類型和標志的類型都是相同的類型,並且來自同一個枚舉。 這張支票有一些費用。

其次,在 HasFlag 內部發生的轉換為UInt64過程中,有一個不幸的框和值的HasFlag 我相信這是由於Enum.HasFlag與所有枚舉Enum.HasFlag工作的要求,無論底層存儲類型如何。

話雖如此, Enum.HasFlag有一個巨大的優勢——它可靠、干凈,並使代碼非常明顯和富有表現力。 在大多數情況下,我認為這值得付出代價 - 但如果您在非常關鍵的性能循環中使用它,則可能值得自己進行檢查。

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編譯為與使用按位比較相同的代碼。

由於本頁討論的裝箱而導致的性能損失也會影響公共.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;

看到返回類型了嗎? 這看起來像一個我們想要使用的函數......但首先考慮 .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之外的任何內容,但您可能希望對此進行改進。 同樣為簡單起見,這(ref-fetches and)每次都直接從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<RuntimeType,ulong[]>的“第一個參數”令人討厭反思。 但是,因為這個“問題” arg 恰好是第一個,所以有一個可愛的解決方法,您可以將每個特定的Enum類型綁定為它自己的委托的Target ,然后每個類型簡化為Func<ulong[]> 。)

顯然,讓這些委托中的任何一個都毫無意義,因為每個委托都只是一個總是返回相同值的函數……但相同的邏輯似乎也適用於原始情況,也許不太明顯(即Func<RuntimeType,ulong[]> )。 盡管我們在這里只使用了一個委托,但您永遠不會真的想為每個 Enum 類型調用一次以上 無論如何,所有這些都導致了一個更好的解決方案,它包含在下面的編輯中。


[編輯:]
這是同一事物的稍微優雅的版本。 如果您將重復調用相同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 -taking 函數必須手動向前傳播類型參數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)。 如前所述,我發現該方法很有用,尤其是考慮到它的簡單性。

JITter 應該將其內聯為一個簡單的按位運算。 JITter 足夠了解甚至可以自定義處理某些框架方法(我認為是通過 MethodImplOptions.InternalCall 嗎?)但 HasFlag 似乎已經逃脫了 Microsoft 的認真關注。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM