Last active
June 7, 2024 16:00
-
-
Save teoadal/95409bc8e0845a46830e29201dfc0b01 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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