From 98acacb83068aa435d75d3e434aeb532522a1e15 Mon Sep 17 00:00:00 2001 From: stephentoub Date: Sat, 30 Jan 2016 11:43:54 -0800 Subject: [PATCH] Improve performance of Enum.Parse/TryParse The allocation profile of Enum.Parse today is fairly poor. While the design of the method returning an Enum requires at least one allocation for the boxed result (the generic TryParse could be overhauled to avoid this, but still has it), additional allocations shouldn't be necessary in the common cases. However, for some reason the current code is boxing a 0 on every call. It's also using string.Trim() to remove whitespace (once for the overall string and then once for each substring), and using String.Split to parse multiple values, which ends up allocating a string[] and an int[] even if there's only one value. This commit removes all allocations from Enum.Parse other than the boxing of the Enum result and some allocations on the code path where Enum.Parse is handed a string containing a number, in which case additional allocations are involved in using the Convert.ChangeType call. With an enum like: ```C# [Flags] enum Colors { Red = 0x1, Orange = 0x2, Yellow = 0x4, Green = 0x8, Blue = 0x10 } ``` using ```Enum.Parse(typeof(Color), "Red")``` repeatedly now results in 5x fewer gen0 GCs and is ~15% faster. Using ```Enum.Parse(typeof(Color), "Red, Orange, Yellow, Green, Blue")``` repeatedly now results in 23x fewer gen0 GCs is is similarly ~15% faster. --- src/mscorlib/src/System/Enum.cs | 89 ++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/src/mscorlib/src/System/Enum.cs b/src/mscorlib/src/System/Enum.cs index 5e2fc37c6711..fa674e74cf12 100644 --- a/src/mscorlib/src/System/Enum.cs +++ b/src/mscorlib/src/System/Enum.cs @@ -17,9 +17,9 @@ namespace System [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { - #region Private Static Data Members - private static readonly char [] enumSeperatorCharArray = new char [] {','}; - private const String enumSeperator = ", "; + #region Private Constants + private const char enumSeparatorChar = ','; + private const String enumSeparatorString = ", "; #endregion #region Private Static Methods @@ -180,7 +180,7 @@ private static String InternalFlagsFormat(RuntimeType eT, Object value) { result -= values[index]; if (!firstTime) - retval.Insert(0, enumSeperator); + retval.Insert(0, enumSeparatorString); retval.Insert(0, names[index]); firstTime = false; @@ -277,11 +277,6 @@ private struct EnumResult internal object m_failureMessageFormatArgument; internal Exception m_innerException; - internal void Init(bool canMethodThrow) - { - parsedEnum = 0; - canThrow = canMethodThrow; - } internal void SetFailure(Exception unhandledException) { m_failure = ParseFailureKind.UnhandledException; @@ -334,7 +329,6 @@ public static bool TryParse(String value, bool ignoreCase, out TEnum resu { result = default(TEnum); EnumResult parseResult = new EnumResult(); - parseResult.Init(false); bool retValue; if (retValue = TryParseEnum(typeof(TEnum), value, ignoreCase, ref parseResult)) @@ -351,8 +345,7 @@ public static Object Parse(Type enumType, String value) [System.Runtime.InteropServices.ComVisible(true)] public static Object Parse(Type enumType, String value, bool ignoreCase) { - EnumResult parseResult = new EnumResult(); - parseResult.Init(true); + EnumResult parseResult = new EnumResult() { canThrow = true }; if (TryParseEnum(enumType, value, ignoreCase, ref parseResult)) return parseResult.parsedEnum; else @@ -377,8 +370,16 @@ private static bool TryParseEnum(Type enumType, String value, bool ignoreCase, r return false; } - value = value.Trim(); - if (value.Length == 0) { + int firstNonWhitespaceIndex = -1; + for (int i = 0; i < value.Length; i++) + { + if (!Char.IsWhiteSpace(value[i])) + { + firstNonWhitespaceIndex = i; + break; + } + } + if (firstNonWhitespaceIndex == -1) { parseResult.SetFailure(ParseFailureKind.Argument, "Arg_MustContainEnumInfo", null); return false; } @@ -387,13 +388,15 @@ private static bool TryParseEnum(Type enumType, String value, bool ignoreCase, r // values will have the first character as as number or a sign. ulong result = 0; - if (Char.IsDigit(value[0]) || value[0] == '-' || value[0] == '+') + char firstNonWhitespaceChar = value[firstNonWhitespaceIndex]; + if (Char.IsDigit(firstNonWhitespaceChar) || firstNonWhitespaceChar == '-' || firstNonWhitespaceChar == '+') { Type underlyingType = GetUnderlyingType(enumType); Object temp; try { + value = value.Trim(); temp = Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture); parseResult.parsedEnum = ToObject(enumType, temp); return true; @@ -415,47 +418,55 @@ private static bool TryParseEnum(Type enumType, String value, bool ignoreCase, r } } - String[] values = value.Split(enumSeperatorCharArray); - - // Find the field.Lets assume that these are always static classes because the class is - // an enum. + // Find the field. Let's assume that these are always static classes + // because the class is an enum. ValuesAndNames entry = GetCachedValuesAndNames(rtType, true); - String[] enumNames = entry.Names; ulong[] enumValues = entry.Values; - - for (int i = 0; i < values.Length; i++) + + StringComparison comparison = ignoreCase ? + StringComparison.OrdinalIgnoreCase : + StringComparison.Ordinal; + + int valueIndex = 0; + while (valueIndex <= value.Length) // '=' is to handle invalid case of an ending comma { - values[i] = values[i].Trim(); // We need to remove whitespace characters + // Find the next separator, if there is one, otherwise the end of the string. + int endIndex = value.IndexOf(enumSeparatorChar, valueIndex); + if (endIndex == -1) + { + endIndex = value.Length; + } - bool success = false; + // Shift the starting and ending indices to eliminate whitespace + int endIndexNoWhitespace = endIndex; + while (valueIndex < endIndex && Char.IsWhiteSpace(value[valueIndex])) valueIndex++; + while (endIndexNoWhitespace > valueIndex && Char.IsWhiteSpace(value[endIndexNoWhitespace - 1])) endIndexNoWhitespace--; + int valueSubstringLength = endIndexNoWhitespace - valueIndex; - for (int j = 0; j < enumNames.Length; j++) + // Try to match this substring against each enum name + bool success = false; + for (int i = 0; i < enumNames.Length; i++) { - if (ignoreCase) - { - if (String.Compare(enumNames[j], values[i], StringComparison.OrdinalIgnoreCase) != 0) - continue; - } - else + if (enumNames[i].Length == valueSubstringLength && + string.Compare(enumNames[i], 0, value, valueIndex, valueSubstringLength, comparison) == 0) { - if (!enumNames[j].Equals(values[i])) - continue; + result |= enumValues[i]; + success = true; + break; } - - ulong item = enumValues[j]; - - result |= item; - success = true; - break; } + // If we couldn't find a match, throw an argument exception. if (!success) { // Not found, throw an argument exception. parseResult.SetFailure(ParseFailureKind.ArgumentWithParameter, "Arg_EnumValueNotFound", value); return false; } + + // Move our pointer to the ending index to go again. + valueIndex = endIndex + 1; } try