Skip to content

Commit fac929d

Browse files
TrayanZapryanovcopybara-github
authored andcommitted
Cache StringBuilder instances in the .NET JsonTextTokenizer.
COPYBARA_INTEGRATE_REVIEW=#15794 from TrayanZapryanov:cache_stringbuilder 596147e PiperOrigin-RevId: 613251480
1 parent 3dadd0e commit fac929d

File tree

1 file changed

+63
-7
lines changed

1 file changed

+63
-7
lines changed

csharp/src/Google.Protobuf/JsonTokenizer.cs

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ private void ValidateState(State validStates, string errorPrefix)
301301
/// </summary>
302302
private string ReadString()
303303
{
304-
var value = new StringBuilder();
304+
//builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire
305+
var builder = StringBuilderCache.Acquire();
305306
bool haveHighSurrogate = false;
306307
while (true)
307308
{
@@ -316,7 +317,7 @@ private string ReadString()
316317
{
317318
throw reader.CreateException("Invalid use of surrogate pair code units");
318319
}
319-
return value.ToString();
320+
return StringBuilderCache.GetStringAndRelease(builder);
320321
}
321322
if (c == '\\')
322323
{
@@ -330,7 +331,7 @@ private string ReadString()
330331
throw reader.CreateException("Invalid use of surrogate pair code units");
331332
}
332333
haveHighSurrogate = char.IsHighSurrogate(c);
333-
value.Append(c);
334+
builder.Append(c);
334335
}
335336
}
336337

@@ -408,7 +409,8 @@ private void ConsumeLiteral(string text)
408409

409410
private double ReadNumber(char initialCharacter)
410411
{
411-
StringBuilder builder = new StringBuilder();
412+
//builder will not be released in case of an exception, but this is not a problem and we will create new on next Acquire
413+
var builder = StringBuilderCache.Acquire();
412414
if (initialCharacter == '-')
413415
{
414416
builder.Append("-");
@@ -437,24 +439,25 @@ private double ReadNumber(char initialCharacter)
437439
}
438440

439441
// TODO: What exception should we throw if the value can't be represented as a double?
442+
var builderValue = StringBuilderCache.GetStringAndRelease(builder);
440443
try
441444
{
442-
double result = double.Parse(builder.ToString(),
445+
double result = double.Parse(builderValue,
443446
NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent,
444447
CultureInfo.InvariantCulture);
445448

446449
// .NET Core 3.0 and later returns infinity if the number is too large or small to be represented.
447450
// For compatibility with other Protobuf implementations the tokenizer should still throw.
448451
if (double.IsInfinity(result))
449452
{
450-
throw reader.CreateException("Numeric value out of range: " + builder);
453+
throw reader.CreateException("Numeric value out of range: " + builderValue);
451454
}
452455

453456
return result;
454457
}
455458
catch (OverflowException)
456459
{
457-
throw reader.CreateException("Numeric value out of range: " + builder);
460+
throw reader.CreateException("Numeric value out of range: " + builderValue);
458461
}
459462
}
460463

@@ -728,6 +731,59 @@ internal InvalidJsonException CreateException(string message)
728731
return new InvalidJsonException(message);
729732
}
730733
}
734+
735+
/// <summary>
736+
/// Provide a cached reusable instance of stringbuilder per thread.
737+
/// Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/StringBuilderCache.cs
738+
/// </summary>
739+
private static class StringBuilderCache
740+
{
741+
private const int MaxCachedStringBuilderSize = 360;
742+
private const int DefaultStringBuilderCapacity = 16; // == StringBuilder.DefaultCapacity
743+
744+
[ThreadStatic]
745+
private static StringBuilder cachedInstance;
746+
747+
/// <summary>Get a StringBuilder for the specified capacity.</summary>
748+
/// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
749+
public static StringBuilder Acquire(int capacity = DefaultStringBuilderCapacity)
750+
{
751+
if (capacity <= MaxCachedStringBuilderSize)
752+
{
753+
StringBuilder sb = cachedInstance;
754+
if (sb != null)
755+
{
756+
// Avoid stringbuilder block fragmentation by getting a new StringBuilder
757+
// when the requested size is larger than the current capacity
758+
if (capacity <= sb.Capacity)
759+
{
760+
cachedInstance = null;
761+
sb.Clear();
762+
return sb;
763+
}
764+
}
765+
}
766+
767+
return new StringBuilder(capacity);
768+
}
769+
770+
/// <summary>Place the specified builder in the cache if it is not too big.</summary>
771+
private static void Release(StringBuilder sb)
772+
{
773+
if (sb.Capacity <= MaxCachedStringBuilderSize)
774+
{
775+
cachedInstance = cachedInstance?.Capacity >= sb.Capacity ? cachedInstance : sb;
776+
}
777+
}
778+
779+
/// <summary>ToString() the stringbuilder, Release it to the cache, and return the resulting string.</summary>
780+
public static string GetStringAndRelease(StringBuilder sb)
781+
{
782+
string result = sb.ToString();
783+
Release(sb);
784+
return result;
785+
}
786+
}
731787
}
732788
}
733789
}

0 commit comments

Comments
 (0)