Skip to content

Instantly share code, notes, and snippets.

@teoadal
Last active June 7, 2024 16:00
Show Gist options
  • Save teoadal/95409bc8e0845a46830e29201dfc0b01 to your computer and use it in GitHub Desktop.
Save teoadal/95409bc8e0845a46830e29201dfc0b01 to your computer and use it in GitHub Desktop.
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ConcurrencyToolkit.Pooling;
using Microsoft.Extensions.ObjectPool;
namespace Bench;
[SimpleJob(RuntimeMoniker.Net80)]
[MeanColumn, MemoryDiagnoser]
public class PoolingBench
{
public const int Threads = 8;
[Benchmark(Baseline = true)]
public int JustObjectPool()
{
var buffer = _buffer;
var impl = _objectPoolImpl;
for (var i = 0; i < buffer.Length; i++)
{
buffer[i] = Task.Run(impl);
}
return SumAndClean(buffer);
}
private int ObjectPoolImpl()
{
var pool = _objectPool;
var instanceA = pool.Get();
var instanceB = pool.Get();
var result =
instanceA.A + instanceA.B + instanceA.C +
instanceB.A + instanceB.B + instanceB.C;
pool.Return(instanceB);
pool.Return(instanceA);
return result;
}
[Benchmark]
public int ConcurrentToolkit()
{
var buffer = _buffer;
var impl = _concurrentToolkitImpl;
for (var i = 0; i < buffer.Length; i++)
{
buffer[i] = Task.Run(impl);
}
return SumAndClean(buffer);
}
private int ConcurrentToolkitImpl()
{
var pool = _concurrentToolkit;
var instanceA = pool.TryRent(out var existsA) ? existsA : new Abc();
var instanceB = pool.TryRent(out var existsB) ? existsB : new Abc();
var result =
instanceA.A + instanceA.B + instanceA.C +
instanceB.A + instanceB.B + instanceB.C;
pool.Return(instanceB);
pool.Return(instanceA);
return result;
}
[Benchmark]
public int ConcurrentToolkitLite()
{
var buffer = _buffer;
var impl = _concurrentToolkitLiteImpl;
for (var i = 0; i < buffer.Length; i++)
{
buffer[i] = Task.Run(impl);
}
return SumAndClean(buffer);
}
private static int ConcurrentToolkitLiteImpl()
{
var instanceA = LiteObjectPool<Abc>.TryRent() ?? new Abc();
var instanceB = LiteObjectPool<Abc>.TryRent() ?? new Abc();
var result =
instanceA.A + instanceA.B + instanceA.C +
instanceB.A + instanceB.B + instanceB.C;
LiteObjectPool<Abc>.Return(instanceB);
LiteObjectPool<Abc>.Return(instanceA);
return result;
}
[Benchmark]
public int Manual()
{
var buffer = _buffer;
var impl = _manualImpl;
for (var i = 0; i < buffer.Length; i++)
{
buffer[i] = Task.Run(impl);
}
return SumAndClean(buffer);
}
private int ManualImpl()
{
var pool = _manual;
var instanceA = pool.Get();
var instanceB = pool.Get();
var result =
instanceA.A + instanceA.B + instanceA.C +
instanceB.A + instanceB.B + instanceB.C;
pool.Return(instanceB);
pool.Return(instanceA);
return result;
}
private static int SumAndClean(Task<int>[] buffer)
{
var result = buffer.Sum(static task => task.Result);
Array.Clear(buffer);
return result;
}
private Task<int>[] _buffer = null!;
private LocklessObjectPool<Abc> _concurrentToolkit = null!;
private Func<int> _concurrentToolkitImpl = null!;
private Func<int> _concurrentToolkitLiteImpl = null!;
private DefaultObjectPool<Abc> _objectPool = null!;
private Func<int> _objectPoolImpl = null!;
private Pool<Abc> _manual = null!;
private Func<int> _manualImpl = null!;
[GlobalSetup]
public void Setup()
{
_buffer = new Task<int>[Threads];
_concurrentToolkit = new LocklessObjectPool<Abc>(
static () => new Abc(),
static clean => clean.TryReset());
_concurrentToolkitImpl = ConcurrentToolkitImpl;
_concurrentToolkitLiteImpl = ConcurrentToolkitLiteImpl;
_objectPool = new DefaultObjectPool<Abc>(new DefaultPooledObjectPolicy<Abc>());
_objectPoolImpl = ObjectPoolImpl;
_manual = new Pool<Abc>(static clean => clean.TryReset());
_manualImpl = ManualImpl;
}
}
public sealed class Abc : IResettable
{
public int A;
public int B;
public int C;
public bool TryReset()
{
A = 0;
B = 0;
C = 0;
return true;
}
}
public sealed class Pool<T>(Func<T, bool> clean, int? capacity = null)
where T : class, IResettable, new()
{
private readonly int _capacity = capacity ?? Environment.ProcessorCount * 2;
private readonly ConcurrentQueue<T> _instances = new();
private T? _fastItem;
private int _length;
public T Get()
{
var result = _fastItem;
if (result != null && Interlocked.CompareExchange(ref _fastItem!, null!, result) == result)
{
return result;
}
if (!_instances.TryDequeue(out var instance))
{
return new T();
}
Interlocked.Decrement(ref _length);
return instance;
}
public bool Return(T instance)
{
if (!clean(instance)) return false;
if (_fastItem == null && Interlocked.CompareExchange(ref _fastItem, instance, null!) == null)
{
return true;
}
if (Interlocked.Increment(ref _length) <= _capacity)
{
_instances.Enqueue(instance);
return true;
}
Interlocked.Decrement(ref _length);
return true;
}
}
/// <summary>
/// https://github.com/epeshk/ConcurrencyToolkit/blob/master/src/ConcurrencyToolkit/Pooling/Internal/LiteObjectPool.cs
/// </summary>
public static class LiteObjectPool<T> where T : class
{
[ThreadStatic] private static T? item;
private static readonly PaddedReference[] Items = new PaddedReference[Environment.ProcessorCount];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? TryRent()
{
T? obj;
// First, try to get an object from TLS if possible.
obj = item;
if (obj is not null)
{
item = null;
return obj;
}
return TryRentRare();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(T obj)
{
if (item is null)
{
item = obj;
return;
}
ReturnRare(obj);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ReturnRare(T obj)
{
ref var preCoreSlot = ref Items[Thread.GetCurrentProcessorId() % (uint)Environment.ProcessorCount];
if (preCoreSlot.Object == null)
Volatile.Write(ref preCoreSlot.Object, obj);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static T? TryRentRare()
{
var obj = Interlocked.Exchange(
ref Items[Thread.GetCurrentProcessorId() % (uint)Environment.ProcessorCount].Object, null);
if (obj is not null)
{
return Unsafe.As<T>(obj);
}
return null;
}
}
[StructLayout(LayoutKind.Explicit, Size = 64)]
internal struct PaddedReference
{
[FieldOffset(0)] public object? Object;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment