Skip to content

Commit

Permalink
Reduce string related allocations in Test262File.FromStream
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Jul 15, 2024
1 parent 9df63bd commit d60382e
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 4 deletions.
148 changes: 148 additions & 0 deletions src/Test262Harness/ObjectPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#nullable disable

using System.Diagnostics;

namespace Test262Harness;

/// <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 ObjectPool<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 ObjectPool(Factory factory)
: this(factory, Environment.ProcessorCount * 2)
{ }

internal ObjectPool(Factory factory, int size)
{
Debug.Assert(size >= 1);
_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.
T inst = _firstItem;
if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst))
{
inst = AllocateSlow();
}

return inst;
}

private T AllocateSlow()
{
var items = _items;

for (var i = 0; i < items.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 = items[i].Value;
if (inst != null)
{
if (inst == Interlocked.CompareExchange(ref items[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)
{
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 items = _items;
for (var i = 0; i < items.Length; i++)
{
if (items[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.
items[i].Value = obj;
break;
}
}
}
}
74 changes: 74 additions & 0 deletions src/Test262Harness/StringBuilderPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#nullable disable

using System.Diagnostics;
using System.Text;

namespace Test262Harness;

/// <summary>
/// The usage is:
/// var inst = StringBuilderPool.GetInstance();
/// var sb = inst.builder;
/// ... Do Stuff...
/// ... sb.ToString() ...
/// inst.Free();
/// </summary>
internal sealed class StringBuilderPool : IDisposable
{
private const int DefaultPoolCapacity = 40 * 1024;
private readonly int _defaultCapacity;

// global pool
private static readonly ObjectPool<StringBuilderPool> s_poolInstance = CreatePool();

public readonly StringBuilder Builder;
private readonly ObjectPool<StringBuilderPool> _pool;

private StringBuilderPool(ObjectPool<StringBuilderPool> pool, int defaultCapacity)
{
Debug.Assert(pool != null);
_defaultCapacity = defaultCapacity;
Builder = new StringBuilder(defaultCapacity);
_pool = pool;
}

public int Length => Builder.Length;

/// <summary>
/// If someone need to create a private pool
/// </summary>
internal static ObjectPool<StringBuilderPool> CreatePool(int size = 100, int capacity = DefaultPoolCapacity)
{
ObjectPool<StringBuilderPool> pool = null;
pool = new ObjectPool<StringBuilderPool>(() => new StringBuilderPool(pool, capacity), size);
return pool;
}

/// <summary>
/// Returns a StringBuilder from the default pool.
/// </summary>
public static StringBuilderPool GetInstance()
{
var builder = s_poolInstance.Allocate();
Debug.Assert(builder.Builder.Length == 0);
return builder;
}

public override string ToString()
{
return Builder.ToString();
}

public void Dispose()
{
var builder = Builder;

// Do not store builders that are too large.

if (builder.Capacity == _defaultCapacity)
{
builder.Clear();
_pool.Free(this);
}
}
}
23 changes: 19 additions & 4 deletions src/Test262Harness/Test262File.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Buffers;
using YamlDotNet.RepresentationModel;

namespace Test262Harness;
Expand Down Expand Up @@ -129,19 +130,33 @@ public static IEnumerable<Test262File> FromStream(Stream stream, string fileName
fileName = NormalizedFilePath(fileName);

string contents;
using (var streamReader = new StreamReader(stream))
const int BufferSize = 4096;
var buffer = ArrayPool<char>.Shared.Rent(BufferSize);
try
{
int count;
using var streamReader = new StreamReader(stream);
using var rawChars = StringBuilderPool.GetInstance();
while ((count = streamReader.ReadBlock(buffer, 0, BufferSize)) > 0)
{
rawChars.Builder.Append(buffer, 0, count);
}

contents = rawChars.ToString();
}
finally
{
contents = streamReader.ReadToEnd();
ArrayPool<char>.Shared.Return(buffer);
}

var yamlStartIndex = contents.IndexOf(YamlSectionStartMarker, StringComparison.OrdinalIgnoreCase);
var yamlStartIndex = contents.IndexOf(YamlSectionStartMarker, StringComparison.Ordinal);

if (yamlStartIndex < 0)
{
throw new ArgumentException($"Test case {fileName} is invalid, cannot find YAML section start.");
}

var yamlEndIndex = contents.IndexOf(YamlSectionEndMarker, yamlStartIndex, StringComparison.OrdinalIgnoreCase);
var yamlEndIndex = contents.IndexOf(YamlSectionEndMarker, yamlStartIndex, StringComparison.Ordinal);

if (yamlEndIndex < 0)
{
Expand Down

0 comments on commit d60382e

Please sign in to comment.