Dzisiaj zaciekawiła mnie informacje, znaleziona w sieci, że ToString() na enum jest bardzo powolne. Postanowiłem to sprawdzić samemu. Załóżmy, że mamy:
public enum Month { January, February, March, April, May, June, July, August, September, October, November, December };
Prosty benchmark, może wyglądać następująco:
int n = 100000; Stopwatch stopwatch = Stopwatch.StartNew(); string text = null; for (int i = 0; i < n; i++) { var month = (Month)(i % 12); text = month.ToString(); } Console.WriteLine(stopwatch.ElapsedTicks);
Wynik:
Taki wynik za wiele nam nie mówi. Spróbujmy napisać drugi przykład, który zamiast enum.ToString(), będzie korzystał z tablicy napisów tzn.:
string[] names = new[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
Wartość enum’a może zostać przeskalowana na indeks w tej tablicy:
stopwatch = Stopwatch.StartNew(); text = null; for (int i = 0; i < n; i++) { var month = (Month) (i%12); text = names[(int)month]; } Console.WriteLine(stopwatch.ElapsedTicks);
Uruchamiając oba benchmarki, zobaczymy:
Różnica podobno jest na tyle duża, że nie można tego traktować jako mikro-optymalizacji. Jeśli mamy kod w jakieś pętli (serwer), to stanowi to zwykłe marnowanie zasobów.
No dobra, to odpowiedzmy sobie na pytanie, skąd taka duża różnica? Zaglądamy oczywiście do IL:
// loop start (head: IL_0025) IL_0010: ldloc.2 IL_0011: ldc.i4.s 12 IL_0013: rem IL_0014: stloc.3 IL_0015: ldloc.3 IL_0016: box ConsoleApplication2.Month IL_001b: callvirt instance string [mscorlib]System.Object::ToString() IL_0020: pop IL_0021: ldloc.2 IL_0022: ldc.i4.1 IL_0023: add IL_0024: stloc.2 IL_0025: ldloc.2 IL_0026: ldloc.0 IL_0027: blt.s IL_0010 // end loop
Widzimy, że ToString jest metodą wirtualną (należącą do Object) a enum jest value type. Oznacza to, że będziemy mieli boxing, co widać po IL. Dla porównania drugie rozwiązanie nie zawiera żadnego boxingu:
// loop start (head: IL_00ce) IL_00bb: ldloc.s i IL_00bd: ldc.i4.s 12 IL_00bf: rem IL_00c0: stloc.s month IL_00c2: ldloc.s names IL_00c4: ldloc.s month IL_00c6: ldelem.ref IL_00c7: pop IL_00c8: ldloc.s i IL_00ca: ldc.i4.1 IL_00cb: add IL_00cc: stloc.s i IL_00ce: ldloc.s i IL_00d0: ldloc.0 IL_00d1: blt.s IL_00bb // end loop
Niestety to nie wszystko. Zagłębmy się dalej do Enum.ToString():
//Enum.ToString() public override string ToString() { return Enum.InternalFormat((RuntimeType)base.GetType(), this.GetValue()); }
Następnie do Enum.InternalFormat:
// System.Enum private static string InternalFormat(RuntimeType eT, object value) { if (eT.IsDefined(typeof(FlagsAttribute), false)) { return Enum.InternalFlagsFormat(eT, value); } string name = Enum.GetName(eT, value); if (name == null) { return value.ToString(); } return name; }
W zależności czy Enum jest opatrzony atrybutem Flags, mamy dwie różne implementacje. O flagach już kiedyś pisałem (zapraszam do lektury).
W naszym przypadku, przechodzimy do Enum.GetName():
public static string GetName(Type enumType, object value) { if (enumType == null) { throw new ArgumentNullException("enumType"); } return enumType.GetEnumName(value); }
Nic tu ciekawego nie ma więc zaglądamy teraz do GetEnumName:
// System.Type /// <summary>Returns the name of the constant that has the specified value, for the current enumeration type.</summary> /// <returns>The name of the member of the current enumeration type that has the specified value, or null if no such constant is found.</returns> /// <param name="value">The value whose name is to be retrieved.</param> /// <exception cref="T:System.ArgumentException">The current type is not an enumeration.-or-<paramref name="value" /> is neither of the current type nor does it have the same underlying type as the current type.</exception> /// <exception cref="T:System.ArgumentNullException"> /// <paramref name="value" /> is null.</exception> public virtual string GetEnumName(object value) { if (value == null) { throw new ArgumentNullException("value"); } if (!this.IsEnum) { throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnum"), "enumType"); } Type type = value.GetType(); if (!type.IsEnum && !Type.IsIntegerType(type)) { throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnumBaseTypeOrEnum"), "value"); } Array enumRawConstantValues = this.GetEnumRawConstantValues(); int num = Type.BinarySearch(enumRawConstantValues, value); if (num >= 0) { string[] enumNames = this.GetEnumNames(); return enumNames[num]; } return null; }
Widzimy znów masę if’ow. Chcemy się jednak bliżej przyjrzeć BinarySearch oraz GetEnumNames. Pierwsza z nich daje nam indeks, a druga nazwy enumów. GetEnumNames wykonuje kilka ifów i wywoła GetEnumData:
// System.Type private void GetEnumData(out string[] enumNames, out Array enumValues) { FieldInfo[] fields = this.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); object[] array = new object[fields.Length]; string[] array2 = new string[fields.Length]; for (int i = 0; i < fields.Length; i++) { array2[i] = fields[i].Name; array[i] = fields[i].GetRawConstantValue(); } IComparer @default = Comparer.Default; for (int j = 1; j < array.Length; j++) { int num = j; string text = array2[j]; object obj = array[j]; bool flag = false; while (@default.Compare(array[num - 1], obj) > 0) { array2[num] = array2[num - 1]; array[num] = array[num - 1]; num--; flag = true; if (num == 0) { break; } } if (flag) { array2[num] = text; array[num] = obj; } } enumNames = array2; enumValues = array; }
Pierwsza rzecz jaką tu widzimy to refleksja, o której wiemy, że nie jest szybka i spowalnia kod. Widzimy, metoda jest bardzo uniwersalna, ale to skutkuje, że jest wolna dla najprostszych przykładów (brak flag i przerw między enumami). Ponadto, drugi parametr wyjściowy, kompletnie jest niepotrzebny dla naszego przypadku (w zasadzie jest ignorowany przez GetEnumNames):
public virtual string[] GetEnumNames() { if (!this.IsEnum) { throw new ArgumentException(Environment.GetResourceString("Arg_MustBeEnum"), "enumType"); } string[] result; Array array; this.GetEnumData(out result, out array); return result; }
BinarySearch znajduje daną wartość korzystając oczywiście z przeszukiwania binarnego:
ulong[] array2 = new ulong[array.Length]; for (int i = 0; i < array.Length; i++) { array2[i] = Enum.ToUInt64(array.GetValue(i)); } ulong value2 = Enum.ToUInt64(value); return Array.BinarySearch<ulong>(array2, value2);
Widzimy zatem, że kod wykonuje bardzo dużo rzeczy (refleksja, warunki, binarne przeszukiwanie, znajdowanie nazwy, boxing). W drugim rozwiązaniu, nie jest potrzebna żadna pętla aby znaleźć konkretną wartość (w końcu już ją mamy).
Warto mieć uwadze powyższe uwagi, wrzucając Enum.ToString gdzieś na serwer. Często przecież ma to charakter niejawny tzn.:
Console.WriteLine(month); // powyzsza linia jest rownowazna z: Console.WriteLine(month.ToString());