Initial OneSizeArrayPool performance improvements.
This commit is contained in:
1 parent
5ee70abf93
commit
4da13cd9c1
6 files changed
+190
-59
No files matched your search
@@ -2,18 +2,85 @@
|
||||
|
||||
namespace Recyclable.Collections.Pools
|
||||
{
|
||||
public sealed class OneSizeArrayPool<T>
|
||||
public sealed class OneSizeArrayPool<T> : ArrayPool<T>
|
||||
{
|
||||
private const int MemoryReleaseTaskSleepMilliseconds = 500;
|
||||
internal bool _memoryExceeded;
|
||||
private bool _disposed;
|
||||
|
||||
~OneSizeArrayPool()
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private async Task ReleaseMemoryOnExcess()
|
||||
{
|
||||
while (!_disposed)
|
||||
{
|
||||
if (_memoryExceeded)
|
||||
{
|
||||
Enter();
|
||||
if (_currentBucket != null)
|
||||
{
|
||||
_blockCount--;
|
||||
_currentBucket = _currentBucket.NextBucket;
|
||||
}
|
||||
|
||||
if (_currentBucket != null)
|
||||
{
|
||||
_blockCount--;
|
||||
_currentBucket = _currentBucket.NextBucket;
|
||||
}
|
||||
|
||||
if (_currentBucket != null)
|
||||
{
|
||||
_blockCount--;
|
||||
_currentBucket = _currentBucket.NextBucket;
|
||||
}
|
||||
|
||||
if (_currentBucket != null)
|
||||
{
|
||||
_blockCount--;
|
||||
_currentBucket = _currentBucket.NextBucket;
|
||||
}
|
||||
|
||||
Exit();
|
||||
}
|
||||
|
||||
await Task.Delay(MemoryReleaseTaskSleepMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly int _blockSize;
|
||||
private readonly int _minBlockCount;
|
||||
private int _blockCount;
|
||||
private MemoryBucket<T>? _currentBucket;
|
||||
private readonly SpinLock _updateLock = new(false);
|
||||
private volatile int _blockCount;
|
||||
private volatile MemoryBucket<T>? _currentBucket;
|
||||
private volatile int _locked;
|
||||
|
||||
private void Enter()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _locked, 1, 0) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SpinWait waiter = new();
|
||||
while (Interlocked.CompareExchange(ref _locked, 1, 0) == 1)
|
||||
{
|
||||
waiter.SpinOnce();
|
||||
}
|
||||
}
|
||||
|
||||
private void Exit()
|
||||
{
|
||||
_locked = 0;
|
||||
}
|
||||
|
||||
public OneSizeArrayPool(int blockSize, int minBlockCount)
|
||||
{
|
||||
_blockSize = blockSize;
|
||||
_minBlockCount = minBlockCount;
|
||||
_ = Task.Run(ReleaseMemoryOnExcess);
|
||||
}
|
||||
|
||||
public override T[] Rent(int minimumLength)
|
||||
@@ -24,8 +91,7 @@ namespace Recyclable.Collections.Pools
|
||||
}
|
||||
|
||||
MemoryBucket<T>? bucket;
|
||||
bool lockTaken = false;
|
||||
_updateLock.Enter(ref lockTaken);
|
||||
Enter();
|
||||
if (_currentBucket != null)
|
||||
{
|
||||
_blockCount--;
|
||||
@@ -37,14 +103,46 @@ namespace Recyclable.Collections.Pools
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
if (lockTaken)
|
||||
Exit();
|
||||
return bucket != null ? bucket.Memory : new T[_blockSize];
|
||||
}
|
||||
|
||||
public T[] Rent()
|
||||
{
|
||||
MemoryBucket<T>? bucket;
|
||||
Enter();
|
||||
if (_currentBucket != null)
|
||||
{
|
||||
_updateLock.Exit(false);
|
||||
_blockCount--;
|
||||
bucket = _currentBucket;
|
||||
_currentBucket = _currentBucket.NextBucket;
|
||||
}
|
||||
else
|
||||
{
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
Exit();
|
||||
return bucket != null ? bucket.Memory : new T[_blockSize];
|
||||
}
|
||||
|
||||
public void ReturnUnchecked(T[] array, bool clearArray)
|
||||
{
|
||||
MemoryBucket<T> bucket = new(array);
|
||||
if (clearArray)
|
||||
{
|
||||
// TODO: Measure performance
|
||||
// new Span<T>(array).Clear();
|
||||
Array.Clear(array, 0, array.Length);
|
||||
}
|
||||
|
||||
Enter();
|
||||
bucket.NextBucket = _currentBucket;
|
||||
_currentBucket = bucket;
|
||||
_blockCount++;
|
||||
Exit();
|
||||
}
|
||||
|
||||
public override void Return(T[] array, bool clearArray = false)
|
||||
{
|
||||
if (array.Length != _blockSize)
|
||||
@@ -55,11 +153,12 @@ namespace Recyclable.Collections.Pools
|
||||
MemoryBucket<T> bucket = new(array);
|
||||
if (clearArray)
|
||||
{
|
||||
// TODO: Measure performance
|
||||
// new Span<T>(array).Clear();
|
||||
Array.Clear(array, 0, array.Length);
|
||||
}
|
||||
|
||||
bool lockTaken = false;
|
||||
_updateLock.Enter(ref lockTaken);
|
||||
Enter();
|
||||
if (_blockCount < _minBlockCount)
|
||||
{
|
||||
bucket.NextBucket = _currentBucket;
|
||||
@@ -67,10 +166,7 @@ namespace Recyclable.Collections.Pools
|
||||
_blockCount++;
|
||||
}
|
||||
|
||||
if (lockTaken)
|
||||
{
|
||||
_updateLock.Exit(false);
|
||||
}
|
||||
Exit();
|
||||
}
|
||||
|
||||
private static void ThrowWrongReturnedArraySizeException(int returnedLength, int blockSize)
|
||||
|
||||
@@ -1,35 +1,84 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Recyclable.Collections.Pools
|
||||
{
|
||||
public static class RecyclableArrayPool<T>
|
||||
public static class RecyclableArrayPool<T>
|
||||
{
|
||||
public static readonly OneSizeArrayPool<T> Null = new(0, 0);
|
||||
|
||||
private static readonly ConcurrentDictionary<int, ArrayPool<T>> _pools = new();
|
||||
|
||||
public static ArrayPool<T> Shared(int blockSize)
|
||||
private enum MemoryPressure
|
||||
{
|
||||
//lock (_lock)
|
||||
High,
|
||||
Medium,
|
||||
Low
|
||||
}
|
||||
|
||||
private static MemoryPressure GetMemoryPressure()
|
||||
{
|
||||
const double HighPressureThreshold = .90; // Percent of GC memory pressure threshold we consider "high"
|
||||
const double MediumPressureThreshold = .70; // Percent of GC memory pressure threshold we consider "medium"
|
||||
|
||||
GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo();
|
||||
|
||||
if (memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * HighPressureThreshold)
|
||||
{
|
||||
return MemoryPressure.High;
|
||||
}
|
||||
|
||||
if (memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * MediumPressureThreshold)
|
||||
{
|
||||
return MemoryPressure.Medium;
|
||||
}
|
||||
|
||||
return MemoryPressure.Low;
|
||||
}
|
||||
|
||||
private static async Task CheckAndReportMemoryPressure()
|
||||
{
|
||||
var previousValue = false;
|
||||
while (true)
|
||||
{
|
||||
if (!_pools.TryGetValue(blockSize, out ArrayPool<T>? pool))
|
||||
bool memoryExceeded = GetMemoryPressure() == MemoryPressure.High;
|
||||
if (memoryExceeded != previousValue)
|
||||
{
|
||||
pool = new OneSizeArrayPool<T>(blockSize, 100);
|
||||
pool = _pools.TryAdd(blockSize, pool) ? pool : _pools[blockSize];
|
||||
previousValue = memoryExceeded;
|
||||
for (var i = 0; i < _pools!.Length; i++)
|
||||
{
|
||||
_pools[i]._memoryExceeded = memoryExceeded;
|
||||
}
|
||||
}
|
||||
|
||||
return pool;
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
static RecyclableArrayPool()
|
||||
{
|
||||
var pools = _pools;
|
||||
for (var arraySize = 0; arraySize < 32; arraySize++)
|
||||
{
|
||||
int blockSize = 2 << arraySize;
|
||||
pools[arraySize] = new OneSizeArrayPool<T>(blockSize, 1000);
|
||||
}
|
||||
|
||||
//_lock.Wait();
|
||||
//try
|
||||
//{
|
||||
//}
|
||||
//finally
|
||||
//{
|
||||
// _lock.Release();
|
||||
//}
|
||||
_ = Task.Run(CheckAndReportMemoryPressure);
|
||||
}
|
||||
|
||||
public static readonly OneSizeArrayPool<T> Null = new(0, 0);
|
||||
|
||||
private static readonly OneSizeArrayPool<T>[] _pools = new OneSizeArrayPool<T>[32];
|
||||
public static T[] Rent(int blockSize) => _pools[31 - BitOperations.LeadingZeroCount((uint)blockSize) - 1].Rent();
|
||||
public static void Return(in T[] memoryBlock, bool needsClearing) => _pools[31 - BitOperations.LeadingZeroCount((uint)memoryBlock.Length) - 1].ReturnUnchecked(memoryBlock, needsClearing);
|
||||
|
||||
// private static readonly ConcurrentDictionary<int, ArrayPool<T>> _pools = new();
|
||||
|
||||
public static OneSizeArrayPool<T> Shared(int blockSize) =>
|
||||
//lock (_lock)
|
||||
//{
|
||||
|
||||
// if (!_pools.TryGetValue(blockSize, out ArrayPool<T>? pool))
|
||||
// {
|
||||
// pool = new OneSizeArrayPool<T>(blockSize, 100);
|
||||
// pool = _pools.TryAdd(blockSize, pool) ? pool : _pools[blockSize];
|
||||
// }
|
||||
_pools[31 - BitOperations.LeadingZeroCount((uint)blockSize) - 1];//}//_lock.Wait();//try//{//}//finally//{// _lock.Release();//}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
public const int BlockSize = 16_384;
|
||||
public const int Capacity = 1;
|
||||
public const long MinItemsCountForParallelization = 850_000;
|
||||
public const int MinPooledArrayLength = 128;
|
||||
public const int MinPooledArrayLength = 37;
|
||||
public const int MaxPooledBlockSize = 2_147_483_591;
|
||||
public const long ItemNotFoundIndexLong = -1L;
|
||||
public const int ItemNotFoundIndex = -1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Recyclable.Collections.Pools;
|
||||
|
||||
namespace Recyclable.Collections
|
||||
{
|
||||
@@ -223,8 +224,6 @@ namespace Recyclable.Collections
|
||||
return values[middle];
|
||||
}
|
||||
|
||||
internal static readonly ArrayPool<T> _arrayPool = ArrayPool<T>.Create();
|
||||
|
||||
private static readonly bool NeedsClearing = !typeof(T).IsValueType;
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
@@ -235,7 +234,7 @@ namespace Recyclable.Collections
|
||||
T[] oldMemoryBlock = list._memoryBlock;
|
||||
list._memoryBlock = (list._capacity <<= 1) < RecyclableDefaults.MinPooledArrayLength
|
||||
? new T[list._capacity]
|
||||
: _arrayPool.Rent(list._capacity);
|
||||
: RecyclableArrayPool<T>.Rent(list._capacity);
|
||||
|
||||
// & WAS SLOWER WITHOUT
|
||||
new Span<T>(oldMemoryBlock).CopyTo(list._memoryBlock);
|
||||
@@ -249,7 +248,7 @@ namespace Recyclable.Collections
|
||||
//}
|
||||
|
||||
// // If anything, it has been already cleared above, so we don't need to repeat it.
|
||||
_arrayPool.Return(oldMemoryBlock, NeedsClearing);
|
||||
RecyclableArrayPool<T>.Return(oldMemoryBlock, NeedsClearing);
|
||||
}
|
||||
|
||||
// list._memoryBlock = newMemoryBlock;
|
||||
@@ -260,7 +259,7 @@ namespace Recyclable.Collections
|
||||
public static void ResizeAndCopy(RecyclableList<T> sourceList, int newSize)
|
||||
{
|
||||
T[] newMemoryBlock = newSize >= RecyclableDefaults.MinPooledArrayLength
|
||||
? _arrayPool.Rent(newSize)
|
||||
? RecyclableArrayPool<T>.Rent(newSize)
|
||||
: new T[newSize];
|
||||
|
||||
// & WAS SLOWER WITHOUT
|
||||
@@ -278,7 +277,7 @@ namespace Recyclable.Collections
|
||||
//}
|
||||
|
||||
// If anything, it has been already cleared above, so we don't need to repeat it.
|
||||
_arrayPool.Return(oldMemoryBlock, NeedsClearing);
|
||||
RecyclableArrayPool<T>.Return(oldMemoryBlock, NeedsClearing);
|
||||
}
|
||||
|
||||
sourceList._memoryBlock = newMemoryBlock;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Recyclable.Collections.Pools;
|
||||
|
||||
namespace Recyclable.Collections
|
||||
{
|
||||
@@ -80,7 +81,7 @@ namespace Recyclable.Collections
|
||||
if (initialCapacity >= InitialCapacity)
|
||||
{
|
||||
_memoryBlock = initialCapacity >= RecyclableDefaults.MinPooledArrayLength
|
||||
? RecyclableListHelpers<T>._arrayPool.Rent(initialCapacity)
|
||||
? RecyclableArrayPool<T>.Rent(initialCapacity)
|
||||
: new T[initialCapacity];
|
||||
_capacity = _memoryBlock.Length;
|
||||
}
|
||||
@@ -138,7 +139,7 @@ namespace Recyclable.Collections
|
||||
if (initialCapacity >= InitialCapacity)
|
||||
{
|
||||
_memoryBlock = initialCapacity >= RecyclableDefaults.MinPooledArrayLength
|
||||
? RecyclableListHelpers<T>._arrayPool.Rent(initialCapacity)
|
||||
? RecyclableArrayPool<T>.Rent(initialCapacity)
|
||||
: new T[initialCapacity];
|
||||
_capacity = _memoryBlock.Length;
|
||||
}
|
||||
@@ -515,7 +516,7 @@ namespace Recyclable.Collections
|
||||
{
|
||||
_capacity = 0;
|
||||
// If anything, it has been already cleared by .Clear(), Remove() or RemoveAt() methods, as the list was modified / disposed.
|
||||
RecyclableListHelpers<T>._arrayPool.Return(_memoryBlock, false);
|
||||
RecyclableArrayPool<T>.Return(_memoryBlock, false);
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -156,15 +156,6 @@ namespace Recyclable.Collections
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static int GetEstimatedBlockSize(int minBlockSize, ArrayPool<T> blockArrayPool)
|
||||
{
|
||||
var array = blockArrayPool.Rent(minBlockSize);
|
||||
minBlockSize = array.Length;
|
||||
blockArrayPool.Return(array);
|
||||
return minBlockSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a set of new memory buffers, if needed, to allow storing at minimum <paramref name="newCapacity"/> no. of items.
|
||||
/// </summary>
|
||||
@@ -220,11 +211,6 @@ namespace Recyclable.Collections
|
||||
{
|
||||
if (minBlockSize >= RecyclableDefaults.MinPooledArrayLength)
|
||||
{
|
||||
if (sourceBlockCount == 0)
|
||||
{
|
||||
minBlockSize = GetEstimatedBlockSize(minBlockSize, blockArrayPool);
|
||||
}
|
||||
|
||||
blockIndex = sourceBlockCount;
|
||||
while (true)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user