diff --git a/csharp/src/Google.Protobuf/JsonTokenizer.cs b/csharp/src/Google.Protobuf/JsonTokenizer.cs index 16daa5626da66..d80eed98bddfa 100644 --- a/csharp/src/Google.Protobuf/JsonTokenizer.cs +++ b/csharp/src/Google.Protobuf/JsonTokenizer.cs @@ -301,7 +301,8 @@ private void ValidateState(State validStates, string errorPrefix) /// private string ReadString() { - var value = new StringBuilder(); + //builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire + var builder = StringBuilderCache.Acquire(); bool haveHighSurrogate = false; while (true) { @@ -316,7 +317,7 @@ private string ReadString() { throw reader.CreateException("Invalid use of surrogate pair code units"); } - return value.ToString(); + return StringBuilderCache.GetStringAndRelease(builder); } if (c == '\\') { @@ -330,7 +331,7 @@ private string ReadString() throw reader.CreateException("Invalid use of surrogate pair code units"); } haveHighSurrogate = char.IsHighSurrogate(c); - value.Append(c); + builder.Append(c); } } @@ -408,7 +409,8 @@ private void ConsumeLiteral(string text) private double ReadNumber(char initialCharacter) { - StringBuilder builder = new StringBuilder(); + //builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire + var builder = StringBuilderCache.Acquire(); if (initialCharacter == '-') { builder.Append("-"); @@ -437,9 +439,10 @@ private double ReadNumber(char initialCharacter) } // TODO: What exception should we throw if the value can't be represented as a double? + var builderValue = StringBuilderCache.GetStringAndRelease(builder); try { - double result = double.Parse(builder.ToString(), + double result = double.Parse(builderValue, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); @@ -447,14 +450,14 @@ private double ReadNumber(char initialCharacter) // For compatibility with other Protobuf implementations the tokenizer should still throw. if (double.IsInfinity(result)) { - throw reader.CreateException("Numeric value out of range: " + builder); + throw reader.CreateException("Numeric value out of range: " + builderValue); } return result; } catch (OverflowException) { - throw reader.CreateException("Numeric value out of range: " + builder); + throw reader.CreateException("Numeric value out of range: " + builderValue); } } @@ -728,6 +731,59 @@ internal InvalidJsonException CreateException(string message) return new InvalidJsonException(message); } } + + /// + /// Provide a cached reusable instance of stringbuilder per thread. + /// Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/StringBuilderCache.cs + /// + private static class StringBuilderCache + { + private const int MaxCachedStringBuilderSize = 360; + private const int DefaultStringBuilderCapacity = 16; // == StringBuilder.DefaultCapacity + + [ThreadStatic] + private static StringBuilder cachedInstance; + + /// Get a StringBuilder for the specified capacity. + /// If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied. + public static StringBuilder Acquire(int capacity = DefaultStringBuilderCapacity) + { + if (capacity <= MaxCachedStringBuilderSize) + { + StringBuilder sb = cachedInstance; + if (sb != null) + { + // Avoid stringbuilder block fragmentation by getting a new StringBuilder + // when the requested size is larger than the current capacity + if (capacity <= sb.Capacity) + { + cachedInstance = null; + sb.Clear(); + return sb; + } + } + } + + return new StringBuilder(capacity); + } + + /// Place the specified builder in the cache if it is not too big. + private static void Release(StringBuilder sb) + { + if (sb.Capacity <= MaxCachedStringBuilderSize) + { + cachedInstance = cachedInstance?.Capacity >= sb.Capacity ? cachedInstance : sb; + } + } + + /// ToString() the stringbuilder, Release it to the cache, and return the resulting string. + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } + } } } }