Che cosa rende Enum.HasFlag così lento?

Stavo facendo alcuni test di velocità e ho notato che Enum.HasFlag è circa 16 volte più lento rispetto all’utilizzo dell’operazione bit a bit.

Qualcuno conosce gli interni di Enum.HasFlag e perché è così lento? Voglio dire che due volte più lentamente non sarebbe male, ma rende la funzione inutilizzabile quando è 16 volte più lenta.

Nel caso qualcuno si stia chiedendo, ecco il codice che sto usando per testare la sua velocità.

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

Risultati: HasFlag: 52ms 53.6ms 55ms Bitwise: 3ms 3ms 3ms

Qualcuno conosce gli interni di Enum.HasFlag e perché è così lento?

Il controllo effettivo è solo un semplice controllo di bit in Enum.HasFlag – non è il problema qui. Detto questo, è più lento del tuo bit di controllo …

Ci sono un paio di motivi per questo rallentamento:

Innanzitutto, Enum.HasFlag esegue un controllo esplicito per assicurarsi che il tipo di enumerazione e il tipo di flag siano entrambi dello stesso tipo e dello stesso Enum. C’è qualche costo in questo assegno.

In secondo luogo, c’è una sfortunata casella e unbox del valore durante una conversione in UInt64 che si verifica all’interno di HasFlag . Questo, ritengo, è dovuto al fatto che Enum.HasFlag funziona con tutte le enumerazioni, indipendentemente dal tipo di archiviazione sottostante.

Detto questo, c’è un enorme vantaggio per Enum.HasFlag : è affidabile, pulito e rende il codice molto ovvio ed espressivo. Per la maggior parte, ritengo che questo ne valga la pena, ma se lo stai utilizzando in un ciclo molto critico, potrebbe valere la pena di fare il tuo controllo.

Il codice decompilato di Enum.HasFlags() aspetto:

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

Se dovessi indovinare, direi che il controllo del tipo era ciò che sta rallentando di più.

Il JITter dovrebbe inserirli come una semplice operazione bit a bit. Il JITter è abbastanza consapevole da gestire anche alcuni metodi di framework (tramite MethodImplOptions.InternalCall, credo?) Ma HasFlag sembra essere sfuggito all’attenzione di Microsoft.

La penalizzazione delle prestazioni dovuta al pugilato discusso in questa pagina riguarda anche le funzioni pubbliche .NET Enum.GetValues e Enum.GetNames , che inoltrano rispettivamente a (Runtime)Type.GetEnumValues e (Runtime)Type.GetEnumNames .

Tutte queste funzioni utilizzano una Array (non generica) come tipo di ritorno, che non è così male per i nomi (poiché String è un tipo di riferimento), ma è piuttosto inappropriato per i valori ulong[] .

Ecco una sbirciata al codice offendente (.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. } 

Possiamo vedere che prima di eseguire la copia, RuntimeType ritorna a System.Enum per ottenere un array interno, un singleton che viene memorizzato nella cache, su richiesta, per ogni Enum specifico. Si noti inoltre che questa versione dell'array dei valori utilizza la firma forte appropriata, ulong[] .

Ecco la funzione .NET (ancora una volta siamo di nuovo in System.Enum ora). C'è una funzione simile per ottenere i nomi (non mostrati).

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

Vedi il tipo di ritorno? Sembra una funzione che vorremmo usare ... Ma prima consideriamo che una seconda ragione per cui .NET esegue nuovamente la copia dell'array ogni volta (come visto sopra) è che .NET deve garantire che ogni chiamante ottenga una copia inalterata dei dati originali, dato che un codificatore malevolo potrebbe cambiare la sua copia della Array restituita, introducendo una corruzione persistente. Pertanto, la precauzione di ricopiatura è intesa in particolare a proteggere la copia master interna memorizzata nella cache.

Se non sei preoccupato di questo rischio, forse perché sei sicuro di non voler cambiare accidentalmente l'array, o forse solo di eliminare alcuni cicli di ottimizzazione (cosa che è sicuramente prematura), è semplice recuperare l'array della cache interna copia dei nomi o dei valori per qualsiasi Enum :

→ Le seguenti due funzioni comprendono il contributo totale di questo articolo ←
→ (ma vedi modifica sotto per una versione migliorata) ←

 static ulong[] GetEnumValues() where T : struct => (ulong[])typeof(System.Enum) .GetMethod("InternalGetValues", BindingFlags.Static | BindingFlags.NonPublic) .Invoke(null, new[] { typeof(T) }); static String[] GetEnumNames() where T : struct => (String[])typeof(System.Enum) .GetMethod("InternalGetNames", BindingFlags.Static | BindingFlags.NonPublic) .Invoke(null, new[] { typeof(T) }); 

Si noti che il vincolo generico su T non è completamente sufficiente per garantire Enum . Per semplicità, ho lasciato fuori controllo oltre la struct , ma potresti voler migliorare. Anche per semplicità, questo (ref-fetches e) riflette direttamente su MethodInfo ogni volta, piuttosto che provare a creare e memorizzare nella cache un Delegate . La ragione di ciò è che la creazione del delegato appropriato con un primo argomento di tipo RuntimeType non pubblico è noioso. Un po 'di più su questo sotto.

Per prima cosa, concluderò con esempi di utilizzo:

 var values = GetEnumValues(); var names = GetEnumNames(); 

e risultati debugger:

 '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" 

Così ho detto che il "primo argomento" di Func è fastidioso per riflettere su. Tuttavia, poiché questo argomento "problema" sembra essere il primo, c'è una soluzione carina in cui è ansible associare ogni specifico tipo Enum come Target del proprio delegato, dove ciascuno viene quindi ridotto a Func .)

Chiaramente, è inutile fare qualcuno di quei delegati, dal momento che ognuno sarebbe solo una funzione che restituisce sempre lo stesso valore ... ma la stessa logica sembra applicarsi, forse meno ovviamente, anche alla situazione originale (cioè, Func ). Anche se ci arriviamo con un solo delegato qui, non vorrai mai chiamarlo più di una volta per tipo Enum . In ogni caso, tutto ciò porta a una soluzione molto migliore, inclusa nella modifica di seguito.


[modificare:]
Ecco una versione leggermente più elegante della stessa cosa. Se chiamerai ripetutamente le funzioni per lo stesso tipo di Enum , la versione mostrata qui utilizzerà la reflection una sola volta per tipo Enum. Salva i risultati in una cache accessibile localmente per un accesso estremamente rapido successivamente.

 static class enum_info_cache 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; }; 

Le due funzioni diventano banali:

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

Il codice mostrato qui illustra uno schema di combinazione di tre trucchi specifici che sembrano mutualmente generare uno schema di cache pigro insolito ed elegante. Ho trovato la tecnica particolare per avere un'applicazione sorprendentemente ampia.

  1. utilizzando una class statica generica per memorizzare nella cache copie indipendenti degli array per ciascun Enum distinto. In particolare, ciò avviene automaticamente e su richiesta;

  2. in relazione a ciò, il blocco del caricatore garantisce un'inizializzazione atomica unica e lo fa senza l'ingombro dei costrutti di controllo condizionale. Possiamo anche proteggere i campi statici con readonly (che, per ovvi motivi, tipicamente non possono essere usati con altri metodi pigri / differiti / richiesti);

  3. infine, possiamo sfruttare l' inferenza di tipo C # per mappare automaticamente la funzione generica (punto di ingresso) nella rispettiva class statica generica, in modo tale che alla fine la cache della domanda sia guidata anche implicitamente ( cioè , il codice migliore è il codice che non è lì - dal momento che non può mai avere bug)

Probabilmente hai notato che l'esempio mostrato qui non illustra molto bene il punto (3). Piuttosto che basarsi sull'inferenza di tipo, la funzione di void del void deve propagare manualmente l'argomento di tipo T Non ho scelto di esporre queste semplici funzioni in modo tale che ci sarebbe l'opportunità di mostrare come l'inferenza di tipo C rende la tecnica complessiva risplendente ...

Tuttavia, si può immaginare che quando si combina una funzione statica generica che può inferire il suo tipo di argomento (s) - cioè, quindi non è nemmeno necessario fornire loro al sito di chiamata - allora diventa abbastanza potente.

L'intuizione chiave è che, mentre le funzioni generiche hanno la piena capacità di inferenza del tipo, le classi generiche no, cioè il compilatore non dedurrà mai T se si tenta di chiamare la prima delle seguenti righe. Ma possiamo ancora ottenere l'accesso completamente dedotto a una class generica e tutti i benefici che ne derivano, attraversandoli tramite la funzione implicita di scrittura generica (ultima riga):

 int t = 4; typed_cache.MyTypedCachedFunc(t); // no inference from 't', explicit type required MyTypedCacheFunc(t); // ok, (but redundant) MyTypedCacheFunc(t); // ok, full inference 

Progettato bene, la tipizzazione desunta può facilmente lanciarti nei dati e nei comportamenti automaticamente memorizzati nella cache, personalizzati per ogni tipo (punti di richiamo 1. e 2). Come notato, trovo l'approccio utile, soprattutto considerando la sua semplicità.