Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce StringBuilderPool #646

Merged
merged 1 commit into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ README.html
samples/dotnet/project.lock.json
YamlDotNet/Properties/AssemblyInfo.Generated.cs

.vs
.vs
.idea

/YamlDotNet/Properties/AssemblyInfo.cs
BenchmarkDotNet.Artifacts

Expand Down
6 changes: 4 additions & 2 deletions YamlDotNet/Core/Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using System.Text;
using System.Text.RegularExpressions;
using YamlDotNet.Core.Events;
using YamlDotNet.Helpers;
using ParsingEvent = YamlDotNet.Core.Events.ParsingEvent;
using TagDirective = YamlDotNet.Core.Tokens.TagDirective;
using VersionDirective = YamlDotNet.Core.Tokens.VersionDirective;
Expand Down Expand Up @@ -1860,11 +1861,12 @@ private void WriteTagContent(string value, bool needsWhitespace)
isIndentation = false;
}

private string UrlEncode(string text)
private static string UrlEncode(string text)
{
return UriReplacer.Replace(text, delegate (Match match)
{
var buffer = new StringBuilder();
using var bufferBuilder = StringBuilderPool.Rent();
var buffer = bufferBuilder.Builder;
foreach (var toEncode in Encoding.UTF8.GetBytes(match.Value))
{
buffer.AppendFormat("%{0:X02}", toEncode);
Expand Down
61 changes: 44 additions & 17 deletions YamlDotNet/Core/Scanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using System.IO;
using System.Text;
using YamlDotNet.Core.Tokens;
using YamlDotNet.Helpers;

namespace YamlDotNet.Core
{
Expand All @@ -35,7 +36,7 @@ public class Scanner : IScanner
{
private const int MaxVersionNumberLength = 9;

private static readonly IDictionary<char, char> SimpleEscapeCodes = new SortedDictionary<char, char>
private static readonly SortedDictionary<char, char> SimpleEscapeCodes = new SortedDictionary<char, char>
{
{ '0', '\0' },
{ 'a', '\x07' },
Expand Down Expand Up @@ -607,7 +608,8 @@ private void ProcessComment()
Skip();
}

var text = new StringBuilder();
using var textBuilder = StringBuilderPool.Rent();
var text = textBuilder.Builder;
while (!analyzer.IsBreakOrZero())
{
text.Append(ReadCurrentCharacter());
Expand Down Expand Up @@ -1260,7 +1262,8 @@ private Token ScanAnchor(bool isAlias)
// '[', ']', '{', '}' and ','
// ref: https://yaml.org/spec/1.2/spec.html#id2785586

var value = new StringBuilder();
using var valueBuilder = StringBuilderPool.Rent();
var value = valueBuilder.Builder;
while (!analyzer.IsWhiteBreakOrZero())
{
// Anchor: read all allowed characters
Expand Down Expand Up @@ -1437,9 +1440,14 @@ private void FetchBlockScalar(bool isLiteral)

Token ScanBlockScalar(bool isLiteral)
{
var value = new StringBuilder();
var leadingBreak = new StringBuilder();
var trailingBreaks = new StringBuilder();
using var valueBuilder = StringBuilderPool.Rent();
var value = valueBuilder.Builder;

using var leadingBreakBuilder = StringBuilderPool.Rent();
var leadingBreak = leadingBreakBuilder.Builder;

using var trailingBreaksBuilder = StringBuilderPool.Rent();
var trailingBreaks = trailingBreaksBuilder.Builder;

var chomping = 0;
var increment = 0;
Expand Down Expand Up @@ -1758,10 +1766,18 @@ private Token ScanFlowScalar(bool isSingleQuoted)

// Consume the content of the quoted scalar.

var value = new StringBuilder();
var whitespaces = new StringBuilder();
var leadingBreak = new StringBuilder();
var trailingBreaks = new StringBuilder();
using var valueBuilder = StringBuilderPool.Rent();
var value = valueBuilder.Builder;

using var whitespacesBuilder = StringBuilderPool.Rent();
var whitespaces = whitespacesBuilder.Builder;

using var leadingBreakBuilder = StringBuilderPool.Rent();
var leadingBreak = leadingBreakBuilder.Builder;

using var trailingBreaksBuilder = StringBuilderPool.Rent();
var trailingBreaks = trailingBreaksBuilder.Builder;

var hasLeadingBlanks = false;

while (true)
Expand Down Expand Up @@ -2009,10 +2025,17 @@ private void FetchPlainScalar()

private Scalar ScanPlainScalar(ref bool isMultiline)
{
var value = new StringBuilder();
var whitespaces = new StringBuilder();
var leadingBreak = new StringBuilder();
var trailingBreaks = new StringBuilder();
using var valueBuilder = StringBuilderPool.Rent();
var value = valueBuilder.Builder;

using var whitespacesBuilder = StringBuilderPool.Rent();
var whitespaces = whitespacesBuilder.Builder;

using var leadingBreakBuilder = StringBuilderPool.Rent();
var leadingBreak = leadingBreakBuilder.Builder;

using var trailingBreaksBuilder = StringBuilderPool.Rent();
var trailingBreaks = trailingBreaksBuilder.Builder;

var hasLeadingBlanks = false;
var currentIndent = indent + 1;
Expand Down Expand Up @@ -2209,7 +2232,8 @@ private void RemoveSimpleKey()
/// </summary>
private string ScanDirectiveName(Mark start)
{
var name = new StringBuilder();
using var nameBuilder = StringBuilderPool.Rent();
var name = nameBuilder.Builder;

// Consume the directive name.

Expand Down Expand Up @@ -2320,7 +2344,9 @@ private Token ScanTagDirectiveValue(Mark start)

private string ScanTagUri(string? head, Mark start)
{
var tag = new StringBuilder();
using var tagBuilder = StringBuilderPool.Rent();
var tag = tagBuilder.Builder;

if (head != null && head.Length > 1)
{
tag.Append(head.Substring(1));
Expand Down Expand Up @@ -2453,7 +2479,8 @@ private string ScanTagHandle(bool isDirective, Mark start)

// Copy the '!' character.

var tagHandle = new StringBuilder();
using var tagHandleBuilder = StringBuilderPool.Rent();
var tagHandle = tagHandleBuilder.Builder;
tagHandle.Append(ReadCurrentCharacter());

// Copy all subsequent alphabetical and numerical characters.
Expand Down
198 changes: 198 additions & 0 deletions YamlDotNet/Helpers/ConcurrentObjectPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// This file is part of YamlDotNet - A .NET library for YAML.
// Copyright (c) Antoine Aubry and contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.


// Adapter from Microsoft code which is
// Copyright (c) Microsoft.
// All Rights Reserved. Licensed under the Apache License, Version 2.0.

using System;
using System.Diagnostics;
using System.Threading;

namespace YamlDotNet.Helpers
{
/// <summary>
/// Generic implementation of object pooling pattern with predefined pool size limit. The main
/// purpose is that limited number of frequently used objects can be kept in the pool for
/// further recycling.
///
/// Notes:
/// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
/// is no space in the pool, extra returned objects will be dropped.
///
/// 2) it is implied that if object was obtained from a pool, the caller will return it back in
/// a relatively short time. Keeping checked out objects for long durations is ok, but
/// reduces usefulness of pooling. Just new up your own.
///
/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
/// Rationale:
/// If there is no intent for reusing the object, do not use pool - just use "new".
/// </summary>
internal sealed class ConcurrentObjectPool<T> where T : class
{
[DebuggerDisplay("{value,nq}")]
private struct Element
{
internal T? value;
}

/// <remarks>
/// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
/// which does not have that type (since it compiles against .NET 2.0).
/// </remarks>
internal delegate T Factory();

// Storage for the pool objects. The first item is stored in a dedicated field because we
// expect to be able to satisfy most requests from it.
private T? firstItem;
private readonly Element[] items;

// factory is stored for the lifetime of the pool. We will call this only when pool needs to
// expand. compared to "new T()", Func gives more flexibility to implementers and faster
// than "new T()".
private readonly Factory factory;

internal ConcurrentObjectPool(Factory factory)
: this(factory, Environment.ProcessorCount * 2)
{
}

internal ConcurrentObjectPool(Factory factory, int size)
{
Debug.Assert(size >= 1);
this.factory = factory;
items = new Element[size - 1];
}

private T CreateInstance()
{
var inst = factory();
return inst;
}

/// <summary>
/// Produces an instance.
/// </summary>
/// <remarks>
/// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
/// Note that Free will try to store recycled objects close to the start thus statistically
/// reducing how far we will typically search.
/// </remarks>
internal T Allocate()
{
// PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
// Note that the initial read is optimistically not synchronized. That is intentional.
// We will interlock only when we have a candidate. in a worst case we may miss some
// recently returned objects. Not a big deal.
var inst = firstItem;
if (inst == null || inst != Interlocked.CompareExchange(ref firstItem, null, inst))
{
inst = AllocateSlow();
}

return inst;
}

private T AllocateSlow()
{
var elements = items;

for (var i = 0; i < elements.Length; i++)
{
// Note that the initial read is optimistically not synchronized. That is intentional.
// We will interlock only when we have a candidate. in a worst case we may miss some
// recently returned objects. Not a big deal.
var inst = elements[i].value;
if (inst != null)
{
if (inst == Interlocked.CompareExchange(ref elements[i].value, null, inst))
{
return inst;
}
}
}

return CreateInstance();
}

/// <summary>
/// Returns objects to the pool.
/// </summary>
/// <remarks>
/// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
/// Note that Free will try to store recycled objects close to the start thus statistically
/// reducing how far we will typically search in Allocate.
/// </remarks>
internal void Free(T obj)
{
Validate(obj);

if (firstItem == null)
{
// Intentionally not using interlocked here.
// In a worst case scenario two objects may be stored into same slot.
// It is very unlikely to happen and will only mean that one of the objects will get collected.
firstItem = obj;
}
else
{
FreeSlow(obj);
}
}

private void FreeSlow(T obj)
{
var elements = items;
for (var i = 0; i < elements.Length; i++)
{
if (elements[i].value == null)
{
// Intentionally not using interlocked here.
// In a worst case scenario two objects may be stored into same slot.
// It is very unlikely to happen and will only mean that one of the objects will get collected.
elements[i].value = obj;
break;
}
}
}

[Conditional("DEBUG")]
private void Validate(object obj)
{
Debug.Assert(obj != null, "freeing null?");

Debug.Assert(firstItem != obj, "freeing twice?");

var elements = items;
for (var i = 0; i < elements.Length; i++)
{
var value = elements[i].value;
if (value == null)
{
return;
}

Debug.Assert(value != obj, "freeing twice?");
}
}
}
}