Add object pooling infrastructure

This change adds types for pooling objects, which can be used to reduce
allocations of temporary, short-lived objects, such as lists and
string builders.
This commit is contained in:
Dustin Campbell 2022-11-23 12:50:05 -08:00
Родитель bf4e7aa069
Коммит df4c0bc754
10 изменённых файлов: 552 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <summary>
/// A pool of <see cref="ImmutableArray{T}.Builder"/> instances.
/// </summary>
///
/// <remarks>
/// Instances originating from this pool are intended to be short-lived and are suitable
/// for temporary work. Do not return them as the results of methods or store them in fields.
/// </remarks>
internal static class ArrayBuilderPool<T>
{
private const int Threshold = 512;
private static readonly Func<ObjectPool<ImmutableArray<T>.Builder>, ImmutableArray<T>.Builder> s_allocate = AllocateAndClear;
private static readonly Action<ObjectPool<ImmutableArray<T>.Builder>, ImmutableArray<T>.Builder> s_release = ClearAndFree;
public static ObjectPool<ImmutableArray<T>.Builder> DefaultPool { get; } = ObjectPool.Default(ImmutableArray.CreateBuilder<T>);
public static PooledObject<ImmutableArray<T>.Builder> GetPooledObject()
=> new(DefaultPool, s_allocate, s_release);
private static ImmutableArray<T>.Builder AllocateAndClear(ObjectPool<ImmutableArray<T>.Builder> pool)
{
var builder = pool.Allocate();
builder.Clear();
return builder;
}
private static void ClearAndFree(ObjectPool<ImmutableArray<T>.Builder> pool, ImmutableArray<T>.Builder builder)
{
if (builder is null)
{
return;
}
builder.Clear();
if (builder.Capacity > Threshold)
{
builder.Capacity = Threshold;
}
pool.Free(builder);
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <summary>
/// A pool of <see cref="HashSet{T}"/> instances that compares items using default equality.
/// </summary>
///
/// <remarks>
/// Instances originating from this pool are intended to be short-lived and are suitable
/// for temporary work. Do not return them as the results of methods or store them in fields.
/// </remarks>
internal static class HashSetPool<T>
{
private const int Threshold = 512;
private static readonly Func<ObjectPool<HashSet<T>>, HashSet<T>> s_allocate = AllocateAndClear;
private static readonly Action<ObjectPool<HashSet<T>>, HashSet<T>> s_release = ClearAndFree;
public static ObjectPool<HashSet<T>> DefaultPool { get; } = ObjectPool.Default<HashSet<T>>();
public static PooledObject<HashSet<T>> GetPooledObject()
=> new(DefaultPool, s_allocate, s_release);
private static HashSet<T> AllocateAndClear(ObjectPool<HashSet<T>> pool)
{
var set = pool.Allocate();
set.Clear();
return set;
}
private static void ClearAndFree(ObjectPool<HashSet<T>> pool, HashSet<T> set)
{
if (set is null)
{
return;
}
var count = set.Count;
set.Clear();
if (count > Threshold)
{
set.TrimExcess();
}
pool.Free(set);
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <summary>
/// A pool of <see cref="List{T}"/> instances.
/// </summary>
///
/// <remarks>
/// Instances originating from this pool are intended to be short-lived and are suitable
/// for temporary work. Do not return them as the results of methods or store them in fields.
/// </remarks>
internal static class ListPool<T>
{
private const int Threshold = 512;
private static readonly Func<ObjectPool<List<T>>, List<T>> s_allocate = AllocateAndClear;
private static readonly Action<ObjectPool<List<T>>, List<T>> s_release = ClearAndFree;
public static ObjectPool<List<T>> DefaultPool { get; } = ObjectPool.Default<List<T>>();
public static PooledObject<List<T>> GetPooledObject()
=> new(DefaultPool, s_allocate, s_release);
private static List<T> AllocateAndClear(ObjectPool<List<T>> pool)
{
var list = pool.Allocate();
list.Clear();
return list;
}
private static void ClearAndFree(ObjectPool<List<T>> pool, List<T> list)
{
if (list is null)
{
return;
}
var count = list.Count;
list.Clear();
if (count > Threshold)
{
list.TrimExcess();
}
pool.Free(list);
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
internal static class ObjectPool
{
private const int DefaultSize = 20;
public static ObjectPool<T> Default<T>()
where T : class, new()
=> DefaultPool<T>.Instance;
public static ObjectPool<T> Default<T>(Func<T> factory)
where T : class
=> new(factory, DefaultSize);
private static class DefaultPool<T>
where T : class, new()
{
public static readonly ObjectPool<T> Instance = new(() => new T(), DefaultSize);
}
}

Просмотреть файл

@ -0,0 +1,142 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
// Copied from https://github/dotnet/roslyn
using System;
using System.Diagnostics;
using System.Threading;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <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.
/// </summary>
///
/// <remarks>
/// <para>Notes:</para>
///
/// <list type="number">
/// <item>
/// It is not the goal to keep all returned objects. The pool is not meant for storage.
/// If there is no space in the pool, extra returned objects will be dropped.
/// </item>
///
/// <item>
/// It is implied that if object was obtained from this pool, the caller will return it back in
/// a relatively short time. Keeping checked out objects for long durations is fine, but reduces
/// the usefulness of pooling. Just new up your own object.
/// </item>
/// </list>
///
/// <para>
/// 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".
/// </para>
/// </remarks>
internal class ObjectPool<T>
where T : class
{
[DebuggerDisplay("{Value,nq}")]
private struct Element
{
public T? _value;
}
// 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;
private readonly Func<T> _factory;
public ObjectPool(Func<T> factory)
: this(factory, Environment.ProcessorCount * 2)
{
}
public ObjectPool(Func<T> factory, int size)
{
_factory = factory;
_items = new Element[size - 1];
}
public ObjectPool(Func<ObjectPool<T>, T> factory, int size)
{
_factory = () => factory(this);
_items = new Element[size - 1];
}
private T CreateInstance() => _factory();
public 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 item = _firstItem;
if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item))
{
item = AllocateSlow();
}
return item;
}
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 item = _items[i]._value;
if (item is not null &&
item == Interlocked.CompareExchange(ref items[i]._value, null, item))
{
return item;
}
}
return CreateInstance();
}
public void Free(T obj)
{
if (_firstItem is 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 is 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;
}
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
// Copied from https://github/dotnet/roslyn
using System;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
internal struct PooledObject<T> : IDisposable
where T : class
{
private readonly ObjectPool<T> _pool;
private readonly Action<ObjectPool<T>, T> _releaser;
private T? _object;
// Because of how this API is intended to be used, we don't want the consumption code to have
// to deal with Object being a nullable reference type. Intead, the guarantee is that this is
// non-null until this is disposed.
public T Object => _object!;
public PooledObject(ObjectPool<T> pool, Func<ObjectPool<T>, T> allocator, Action<ObjectPool<T>, T> releaser)
: this()
{
_pool = pool;
_object = allocator(pool);
_releaser = releaser;
}
public void Dispose()
{
if (_object is { } obj)
{
_releaser(_pool, obj);
_object = null;
}
}
}

Просмотреть файл

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Utilities;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <summary>
/// A pool of <see cref="HashSet{T}"/> instances that compares items using reference equality.
/// </summary>
///
/// <remarks>
/// Instances originating from this pool are intended to be short-lived and are suitable
/// for temporary work. Do not return them as the results of methods or store them in fields.
/// </remarks>
internal static class ReferenceEqualityHashSetPool<T>
where T : class
{
private const int Threshold = 512;
private static readonly Func<ObjectPool<HashSet<T>>, HashSet<T>> s_allocate = AllocateAndClear;
private static readonly Action<ObjectPool<HashSet<T>>, HashSet<T>> s_release = ClearAndFree;
public static ObjectPool<HashSet<T>> DefaultPool { get; } = ObjectPool.Default(() => new HashSet<T>(ReferenceEqualityComparer<T>.Instance));
public static PooledObject<HashSet<T>> GetPooledObject()
=> new(DefaultPool, s_allocate, s_release);
private static HashSet<T> AllocateAndClear(ObjectPool<HashSet<T>> pool)
{
var set = pool.Allocate();
set.Clear();
return set;
}
private static void ClearAndFree(ObjectPool<HashSet<T>> pool, HashSet<T> set)
{
if (set is null)
{
return;
}
var count = set.Count;
set.Clear();
if (count > Threshold)
{
set.TrimExcess();
}
pool.Free(set);
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <summary>
/// A pool of <see cref="Stack{T}"/> instances.
/// </summary>
///
/// <remarks>
/// Instances originating from this pool are intended to be short-lived and are suitable
/// for temporary work. Do not return them as the results of methods or store them in fields.
/// </remarks>
internal static class StackPool<T>
{
private const int Threshold = 512;
private static readonly Func<ObjectPool<Stack<T>>, Stack<T>> s_allocate = AllocateAndClear;
private static readonly Action<ObjectPool<Stack<T>>, Stack<T>> s_release = ClearAndFree;
public static ObjectPool<Stack<T>> DefaultPool { get; } = ObjectPool.Default<Stack<T>>();
public static PooledObject<Stack<T>> GetPooledObject()
=> new(DefaultPool, s_allocate, s_release);
private static Stack<T> AllocateAndClear(ObjectPool<Stack<T>> pool)
{
var stack = pool.Allocate();
stack.Clear();
return stack;
}
private static void ClearAndFree(ObjectPool<Stack<T>> pool, Stack<T> stack)
{
if (stack is null)
{
return;
}
var count = stack.Count;
stack.Clear();
if (count > Threshold)
{
stack.TrimExcess();
}
pool.Free(stack);
}
}

Просмотреть файл

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Text;
namespace Microsoft.AspNetCore.Razor.PooledObjects;
/// <summary>
/// A pool of <see cref="StringBuilder"/> instances.
/// </summary>
///
/// <remarks>
/// Instances originating from this pool are intended to be short-lived and are suitable
/// for temporary work. Do not return them as the results of methods or store them in fields.
/// </remarks>
internal static class StringBuilderPool
{
private const int Threshold = 512;
private static readonly Func<ObjectPool<StringBuilder>, StringBuilder> s_allocate = AllocateAndClear;
private static readonly Action<ObjectPool<StringBuilder>, StringBuilder> s_release = ClearAndFree;
public static ObjectPool<StringBuilder> DefaultPool { get; } = ObjectPool.Default<StringBuilder>();
public static PooledObject<StringBuilder> GetPooledObject()
=> new(DefaultPool, s_allocate, s_release);
private static StringBuilder AllocateAndClear(ObjectPool<StringBuilder> pool)
{
var builder = pool.Allocate();
builder.Clear();
return builder;
}
private static void ClearAndFree(ObjectPool<StringBuilder> pool, StringBuilder builder)
{
if (builder is null)
{
return;
}
builder.Clear();
if (builder.Capacity > Threshold)
{
builder.Capacity = Threshold;
}
pool.Free(builder);
}
}

Просмотреть файл

@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Razor.Utilities;
internal class ReferenceEqualityComparer<T> : IEqualityComparer<T>
where T : class
{
public static readonly ReferenceEqualityComparer<T> Instance = new();
private ReferenceEqualityComparer()
{
}
bool IEqualityComparer<T>.Equals(T? x, T? y)
=> ReferenceEquals(x, y);
int IEqualityComparer<T>.GetHashCode(T obj)
=> RuntimeHelpers.GetHashCode(obj);
}