[英]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.GetValues
和Enum.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;
此處顯示的代碼說明了一種組合三個特定技巧的模式,這些技巧似乎相互產生了異常優雅的惰性緩存方案。 我發現這種特殊的技術有着驚人的廣泛應用。
使用通用靜態類為每個不同的Enum
緩存數組的獨立副本。 值得注意的是,這是自動和按需發生的;
與此相關的是, 加載器鎖保證了唯一的原子初始化,並且不會出現條件檢查結構的混亂。 我們還可以使用readonly
保護靜態字段(出於顯而易見的原因,通常不能與其他惰性/延遲/需求方法一起使用);
最后,我們可以利用 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.