RecyclableList.IndexOf performance improvements.

This commit is contained in:
mlemanczyk committed 2023-05-21 15:42:32 +02:00
1 parent 209dac1808
commit b458f450fc
10 files changed
+502 -309

No files matched your search

+1 -3
View File
@@ -5,7 +5,6 @@ namespace Recyclable.Collections
public sealed class ItemRange public sealed class ItemRange
{ {
public int BlockIndex; public int BlockIndex;
public int StartingItemIndex;
public long ItemsToSearchCount; public long ItemsToSearchCount;
internal bool _returned; internal bool _returned;
@@ -13,11 +12,10 @@ namespace Recyclable.Collections
{ {
} }
public ItemRange(int blockIndex, int startingItemIndex, long itemsToSearchCount) public ItemRange(int blockIndex, long itemsToSearchCount)
{ {
BlockIndex = blockIndex; BlockIndex = blockIndex;
ItemsToSearchCount = itemsToSearchCount; ItemsToSearchCount = itemsToSearchCount;
StartingItemIndex = startingItemIndex;
} }
public void Dispose() public void Dispose()
+5 -14
View File
@@ -8,31 +8,22 @@ namespace Recyclable.Collections
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.NoInlining)]
public static void Iterate<TList, T>(in ParallelSynchronizationContext context, in SearchConfiguration<TList, T> search, Func<ParallelSynchronizationContext, SearchConfiguration<TList, T>, ItemRange, bool> action) public static void Iterate<TList, T>(in ParallelSynchronizationContext context, in SearchConfiguration<TList, T> search, Func<ParallelSynchronizationContext, SearchConfiguration<TList, T>, ItemRange, bool> action)
{ {
int itemIndex = 0;
ItemRange itemRange; ItemRange itemRange;
int blockIndex = search.StartingBlockIndex; int blockIndex = search.StartingBlockIndex;
long itemsCount = search.ItemsCount; long itemsCount = search.ItemsCount;
while (itemsCount > search.Step) while (itemsCount > search.BlockSize)
{ {
itemRange = new(blockIndex, itemIndex, search.Step); itemRange = new(blockIndex, search.BlockSize);
if (!action(context, search, itemRange)) if (!action(context, search, itemRange))
{ {
break; break;
} }
itemsCount -= search.Step; itemsCount -= search.BlockSize;
//blockIndex = (int)Math.DivRem(itemIndex + step, blockSize, out itemIndex); blockIndex++;
blockIndex += (int)((itemIndex + search.Step) >> search.BlockSizePow2BitShift);
itemIndex = (int)((itemIndex + search.Step) & search.BlockSizeMinus1);
//itemIndex += step;
//while (itemIndex >= blockSize)
//{
// blockIndex++;
// itemIndex -= blockSize;
//}
} }
itemRange = new(blockIndex, itemIndex, itemsCount); itemRange = new(blockIndex, itemsCount);
_ = action(context, search, itemRange); _ = action(context, search, itemRange);
} }
} }
@@ -2,7 +2,7 @@ namespace Recyclable.Collections.Parallel
{ {
public sealed class ParallelSynchronizationContext : IDisposable public sealed class ParallelSynchronizationContext : IDisposable
{ {
public readonly ManualResetEventSlimmer ItemFoundSignal; internal bool _isItemFound;
public readonly Barrier AllDoneSignal; public readonly Barrier AllDoneSignal;
public long FoundItemIndex; public long FoundItemIndex;
@@ -11,16 +11,21 @@ namespace Recyclable.Collections.Parallel
public ParallelSynchronizationContext() public ParallelSynchronizationContext()
{ {
ItemFoundSignal = ManualResetEventSlimmerPool.Create(false); AllDoneSignal = new(0);
AllDoneSignal = new(0);
} }
public ParallelSynchronizationContext(int participantCount) public ParallelSynchronizationContext(int participantCount)
{ {
ItemFoundSignal = ManualResetEventSlimmerPool.Create(false);
AllDoneSignal = new(participantCount); AllDoneSignal = new(participantCount);
} }
public void SetItemFound()
{
_isItemFound = true;
}
public bool IsItemFound => _isItemFound;
public void Dispose() public void Dispose()
{ {
if (!_returned) if (!_returned)
@@ -30,7 +35,6 @@ namespace Recyclable.Collections.Parallel
} }
else else
{ {
ItemFoundSignal.Dispose();
AllDoneSignal.Dispose(); AllDoneSignal.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
@@ -17,9 +17,9 @@ namespace Recyclable.Collections.Parallel
_updateLock.Exit(false); _updateLock.Exit(false);
} }
if (context.ItemFoundSignal._isSet) if (context._isItemFound)
{ {
context.ItemFoundSignal.Reset(); context._isItemFound = false;
} }
switch (context.AllDoneSignal.ParticipantsRemaining) switch (context.AllDoneSignal.ParticipantsRemaining)
@@ -40,7 +40,7 @@ namespace Recyclable.Collections.Parallel
if (context.FoundItemIndex >= 0) if (context.FoundItemIndex >= 0)
{ {
context.FoundItemIndex = -1L; context.FoundItemIndex = RecyclableDefaults.ItemNotFoundIndexLong;
} }
context._returned = false; context._returned = false;
@@ -7,7 +7,7 @@ namespace Recyclable.Collections.Pools
private static readonly SpinLock _updateLock = new(false); private static readonly SpinLock _updateLock = new(false);
public static readonly ObjectPool<ItemRange> Shared = new DefaultObjectPool<ItemRange>(new DefaultPooledObjectPolicy<ItemRange>()); public static readonly ObjectPool<ItemRange> Shared = new DefaultObjectPool<ItemRange>(new DefaultPooledObjectPolicy<ItemRange>());
public static ItemRange Create(int blockIndex, int startingItemIndex, long itemsToSearchCount) public static ItemRange Create(int blockIndex, long itemsToSearchCount)
{ {
bool lockTaken = false; bool lockTaken = false;
_updateLock.Enter(ref lockTaken); _updateLock.Enter(ref lockTaken);
@@ -20,7 +20,6 @@ namespace Recyclable.Collections.Pools
itemRange._returned = false; itemRange._returned = false;
itemRange.BlockIndex = blockIndex; itemRange.BlockIndex = blockIndex;
itemRange.ItemsToSearchCount = itemsToSearchCount; itemRange.ItemsToSearchCount = itemsToSearchCount;
itemRange.StartingItemIndex = startingItemIndex;
return itemRange; return itemRange;
} }
} }
@@ -4,7 +4,10 @@
{ {
public const int BlockSize = 16_384; public const int BlockSize = 16_384;
public const int Capacity = 1; public const int Capacity = 1;
public const long MinItemsCountForParallelization = 786_432;
public const int MinPooledArrayLength = 128; public const int MinPooledArrayLength = 128;
public const int MaxPooledBlockSize = 2_147_483_591; public const int MaxPooledBlockSize = 2_147_483_591;
public const long ItemNotFoundIndexLong = -1L;
public const int ItemNotFoundIndex = -1;
} }
} }
@@ -0,0 +1,295 @@
using System.Runtime.CompilerServices;
using Recyclable.Collections.Parallel;
namespace Recyclable.Collections
{
public partial class RecyclableList<T>
{
public static class RecyclableListIndexOfHelpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.NoOptimization)]
private static void ScheduleIndexOfTask(ParallelSynchronizationContext context, RecyclableList<T> list, int blockIndex, T itemToFind)
{
_ = context.AllDoneSignal.AddParticipant();
// & WAS SLOWER
// _ = ThreadPool.QueueUserWorkItem<object?>((_) =>
_ = Task.Factory.StartNew(() =>
{
try
{
if (context._isItemFound)
{
return;
}
long index = Array.IndexOf(list._memoryBlocks[blockIndex], itemToFind, 0, list._blockSize);
if (context._isItemFound)
{
return;
}
else if (index >= 0)
{
context._isItemFound = true;
_ = Interlocked.Exchange(ref context.FoundItemIndex, (blockIndex << list._blockSizePow2BitShift) + index);
return;
}
index = Array.IndexOf(list._memoryBlocks[blockIndex + 1], itemToFind, 0, list._blockSize);
if (context._isItemFound)
{
return;
}
else if (index >= 0)
{
context._isItemFound = true;
_ = Interlocked.Exchange(ref context.FoundItemIndex, ((blockIndex + 1) << list._blockSizePow2BitShift) + index);
return;
}
index = Array.IndexOf(list._memoryBlocks[blockIndex + 2], itemToFind, 0, list._blockSize);
if (context._isItemFound)
{
return;
}
else if (index >= 0)
{
context._isItemFound = true;
_ = Interlocked.Exchange(ref context.FoundItemIndex, ((blockIndex + 2) << list._blockSizePow2BitShift) + index);
return;
}
index = Array.IndexOf(list._memoryBlocks[blockIndex + 3], itemToFind, 0, list._blockSize);
if (!context._isItemFound && index >= 0)
{
context._isItemFound = true;
_ = Interlocked.Exchange(ref context.FoundItemIndex, ((blockIndex + 3) << list._blockSizePow2BitShift) + index);
}
//itemsToSearch -= blockSize;
//if (itemsToSearch > 0)
//{
// if (itemsToSearch < blockSize)
// {
// blockSize = (int)itemsToSearch;
// }
// index = Array.IndexOf(list._memoryBlocks[blockIndex + 2], itemToFind, 0, blockSize);
// if (index >= 0)
// {
// if (!context.ItemFoundSignal.IsSet)
// {
// context.ItemFoundSignal.Set();
// _ = Interlocked.Exchange(ref context.FoundItemIndex, ((blockIndex + 2) << list._blockSizePow2BitShift) + index);
// }
// return;
// }
//}
//itemsToSearch -= blockSize;
//if (itemsToSearch > 0)
//{
// if (itemsToSearch < blockSize)
// {
// blockSize = (int)itemsToSearch;
// }
// index = Array.IndexOf(list._memoryBlocks[blockIndex + 3], itemToFind, 0, blockSize);
// if (!context.ItemFoundSignal.IsSet && index >= 0)
// {
// context.ItemFoundSignal.Set();
// _ = Interlocked.Exchange(ref context.FoundItemIndex, ((blockIndex + 3) << list._blockSizePow2BitShift) + index);
// return;
// }
//}
}
catch (Exception e)
{
e.Data.Add("list", list);
e.Data.Add("itemToFind", itemToFind);
e.Data.Add("blockIndex", blockIndex);
_ = Interlocked.Exchange(ref context.Exception, e);
}
finally
{
context.AllDoneSignal.RemoveParticipant();
}
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.NoOptimization)]
private static void ScheduleIndexOfTaskSingle(ParallelSynchronizationContext context, RecyclableList<T> list, int blockIndex, T itemToFind)
{
_ = context.AllDoneSignal.AddParticipant();
// & WAS SLOWER
// _ = ThreadPool.QueueUserWorkItem<object?>((_) =>
_ = Task.Factory.StartNew(() =>
{
try
{
if (context._isItemFound)
{
return;
}
long index = Array.IndexOf(list._memoryBlocks[blockIndex], itemToFind, 0, list._blockSize);
if (context._isItemFound)
{
return;
}
else if (index >= 0)
{
context._isItemFound = true;
_ = Interlocked.Exchange(ref context.FoundItemIndex, (blockIndex << list._blockSizePow2BitShift) + index);
}
}
catch (Exception e)
{
_ = Interlocked.Exchange(ref context.Exception, e);
}
finally
{
context.AllDoneSignal.RemoveParticipant();
}
});
}
private const int BlocksForEachTask = 4;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static long IndexOfParallel(RecyclableList<T> list, in T item)
{
// & WAS SLOWER
//long itemsCount = Array.IndexOf(list._memoryBlocks[0], item, 0, (int)Math.Min(list._blockSize, list._longCount));
//if (itemsCount >= 0)
//{
// return itemsCount;
//}
//using var context = new ParallelSynchronizationContext(1);
ParallelSynchronizationContext context = ParallelSynchronizationContextPool.GetWithOneParticipant();
// (long)(_longCount * 0.329) => most efficient step, based on benchmarks
//Iterate(this, context, item);
// TODO: Switch to use _blockSize instead of list._longCountIndeexOfStep for simplifications & better performance.
// & WAS SLOWER without
int blockIndex = 2;
// & WAS SLOWER
//long itemsCount = list._longCount - list._blockSize;
// & WAS SLOWER
// int blockSize = _blockSize;
// & WAS SLOWER
// while ((blockIndex << list._blockSizePow2BitShift) + itemIndex > step)
//long blockSize = list._blockSize << 1;
int lastBlockWithData = list._lastBlockWithData - BlocksForEachTask;
while (blockIndex < lastBlockWithData)
{
// At this point itemIndex is limited to blockSize range - int.
ScheduleIndexOfTask(context, list, blockIndex, item);
if (context._isItemFound)
{
break;
}
blockIndex += BlocksForEachTask;
}
if (!context._isItemFound)
{
while (blockIndex < list._lastBlockWithData)
{
ScheduleIndexOfTaskSingle(context, list, blockIndex, item);
if (context._isItemFound)
{
break;
}
blockIndex++;
}
}
int itemIndex = Array.IndexOf(list._memoryBlocks[list._lastBlockWithData], item, 0, list._nextItemIndex > 0 ? list._nextItemIndex : list._blockSize);
if (itemIndex >= 0)
{
context._isItemFound = true;
_ = Interlocked.Exchange(ref context.FoundItemIndex, ((long)blockIndex << list._blockSizePow2BitShift) + itemIndex);
}
context.AllDoneSignal.SignalAndWait();
if (context.Exception == null)
{
return context.FoundItemIndex;
}
context.Exception.CaptureAndRethrow();
return RecyclableDefaults.ItemNotFoundIndexLong;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static long IndexOfSequential(RecyclableList<T> list, in T item) //, in T[][] memoryBlocks, int lastBlockIndex, int blockSize, int nextItemIndex)
{
int itemIndex;
// & WAS SLOWER
//int lastBlockWithData = list._lastBlockWithData;
Span<T[]> memoryBlocksSpan = new(list._memoryBlocks, 0, list._lastBlockWithData + 1);
int blockIndex;
for (blockIndex = 1; blockIndex < list._lastBlockWithData; blockIndex += 4)
{
itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, list._blockSize);
if (itemIndex >= 0)
{
return itemIndex + ((long)blockIndex << list._blockSizePow2BitShift);
}
if (blockIndex + 1 < list._lastBlockWithData)
{
itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 1], item, 0, list._blockSize);
if (itemIndex >= 0)
{
return itemIndex + ((long)(blockIndex + 1)<< list._blockSizePow2BitShift);
}
if (blockIndex + 2 < list._lastBlockWithData)
{
itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 2], item, 0, list._blockSize);
if (itemIndex >= 0)
{
return itemIndex + ((long)(blockIndex + 2)<< list._blockSizePow2BitShift);
}
if (blockIndex + 3 < list._lastBlockWithData)
{
itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex + 3], item, 0, list._blockSize);
if (itemIndex >= 0)
{
return itemIndex + ((long)(blockIndex + 3)<< list._blockSizePow2BitShift);
}
}
}
}
}
//if (blockIndex == list._lastBlockWithData)
//{
itemIndex = list._nextItemIndex != 0
? Array.IndexOf(memoryBlocksSpan[list._lastBlockWithData], item, 0, list._nextItemIndex)
: Array.IndexOf(memoryBlocksSpan[list._lastBlockWithData], item, 0, list._blockSize);
//}
//else
//{
// return RecyclableDefaults.ItemNotFoundIndexLong;
//}
return itemIndex >= 0 ? itemIndex + ((long)list._lastBlockWithData << list._blockSizePow2BitShift) : RecyclableDefaults.ItemNotFoundIndexLong;
}
public static int Min(int a, int b, int c) => a < b ? a < c ? a : c : b < c ? b : c;
}
}
}
+126 -223
View File
@@ -2,19 +2,14 @@
using System.Collections; using System.Collections;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Recyclable.Collections.Parallel;
using Recyclable.Collections.Pools; using Recyclable.Collections.Pools;
[assembly: InternalsVisibleTo("Recyclable.Collections.Benchmarks")] [assembly: InternalsVisibleTo("Recyclable.Collections.Benchmarks")]
namespace Recyclable.Collections namespace Recyclable.Collections
{ {
public class RecyclableList<T> : IDisposable, IList<T> public partial class RecyclableList<T> : IDisposable, IList<T>
{ {
private const long ItemNotFoundIndexLong = -1L;
private const int ItemNotFoundIndex = -1;
private const double OptimalParallelSearchStep = 0.1;// 0.329;
private static readonly ArrayPool<T[]> _defaultMemoryBlocksPool = ArrayPool<T[]>.Create(); private static readonly ArrayPool<T[]> _defaultMemoryBlocksPool = ArrayPool<T[]>.Create();
private static readonly ArrayPool<T> _defaultBlockArrayPool = ArrayPool<T>.Create(); private static readonly ArrayPool<T> _defaultBlockArrayPool = ArrayPool<T>.Create();
private static readonly T[][] _emptyMemoryBlocksArray = new T[0][]; private static readonly T[][] _emptyMemoryBlocksArray = new T[0][];
@@ -24,7 +19,7 @@ namespace Recyclable.Collections
internal byte _blockSizePow2BitShift; internal byte _blockSizePow2BitShift;
internal int _blockSizeMinus1; internal int _blockSizeMinus1;
private int _nextItemBlockIndex; private int _nextItemBlockIndex;
private int _nextItemIndex; internal int _nextItemIndex;
private int _reservedBlockCount; private int _reservedBlockCount;
protected static readonly bool NeedsClearing = !typeof(T).IsValueType; protected static readonly bool NeedsClearing = !typeof(T).IsValueType;
@@ -42,19 +37,15 @@ namespace Recyclable.Collections
public int Count => checked((int)_longCount); public int Count => checked((int)_longCount);
public bool IsReadOnly { get; } public bool IsReadOnly { get; }
public int LastBlockWithData => _nextItemBlockIndex - (_nextItemIndex > 0 ? 0 : 1); private int _lastBlockWithData;
public int LastBlockWithData => _lastBlockWithData;
internal long _longCountIndexOfStep;
internal long _longCount; internal long _longCount;
public long LongCount public long LongCount
{ {
get => _longCount; get => _longCount;
set set => _longCount = value;
{
_longCount = value;
_longCountIndexOfStep = Math.Max((long)(_longCount * OptimalParallelSearchStep), 1L);
}
} }
public int ReservedBlockCount => _reservedBlockCount; public int ReservedBlockCount => _reservedBlockCount;
@@ -75,7 +66,7 @@ namespace Recyclable.Collections
int targetBlockIndex = (int)(index >> list._blockSizePow2BitShift); int targetBlockIndex = (int)(index >> list._blockSizePow2BitShift);
int targetItemIndex = (int)(index & list._blockSizeMinus1); int targetItemIndex = (int)(index & list._blockSizeMinus1);
int lastTakenBlockIndex = list.LastBlockWithData; int lastTakenBlockIndex = list._lastBlockWithData;
while (sourceBlockIndex < lastTakenBlockIndex || (sourceBlockIndex == lastTakenBlockIndex && (sourceItemIndex < nextItemIndex || sourceBlockIndex != list._nextItemBlockIndex))) while (sourceBlockIndex < lastTakenBlockIndex || (sourceBlockIndex == lastTakenBlockIndex && (sourceItemIndex < nextItemIndex || sourceBlockIndex != list._nextItemBlockIndex)))
{ {
int toCopy = sourceBlockIndex < lastTakenBlockIndex || nextItemIndex == 0 int toCopy = sourceBlockIndex < lastTakenBlockIndex || nextItemIndex == 0
@@ -150,6 +141,8 @@ namespace Recyclable.Collections
if (!enumerator.MoveNext()) if (!enumerator.MoveNext())
{ {
_lastBlockWithData = targetBlockIdx;
_longCount += copied + i - _longCount;
if (targetItemIdx < blockSize) if (targetItemIdx < blockSize)
{ {
_nextItemBlockIndex = targetBlockIdx; _nextItemBlockIndex = targetBlockIdx;
@@ -161,8 +154,6 @@ namespace Recyclable.Collections
_nextItemIndex = 0; _nextItemIndex = 0;
} }
_longCount += copied + i - _longCount;
_longCountIndexOfStep = Math.Max((long)(_longCount * OptimalParallelSearchStep), 1L);
return; return;
} }
@@ -220,153 +211,9 @@ namespace Recyclable.Collections
} }
_longCount = requiredCapacity; _longCount = requiredCapacity;
_longCountIndexOfStep = Math.Max((long)(requiredCapacity * OptimalParallelSearchStep), 1L);
_nextItemBlockIndex = targetBlockIdx; _nextItemBlockIndex = targetBlockIdx;
_nextItemIndex = targetItemIdx; _nextItemIndex = targetItemIdx;
} _lastBlockWithData = targetBlockIdx - (targetItemIdx > 0 ? 0 : 1);
// & WAS SLOWER
// [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
private static long DoLongIndexOfParallel(RecyclableList<T> list, in ItemRange itemRange, in T itemToFind, in ManualResetEventSlimmer itemFoundSignal)
{
// & WAS SLOWER without
int blockSize = list._blockSize;
int blockIndex = itemRange.BlockIndex;
long itemsToSearchCount = itemRange.ItemsToSearchCount;
int index = Array.IndexOf(list._memoryBlocks[blockIndex], itemToFind, itemRange.StartingItemIndex, (int)Math.Min(blockSize - itemRange.StartingItemIndex, itemsToSearchCount));
if (itemFoundSignal.IsSet)
{
return ItemNotFoundIndexLong;
}
else if (index >= 0)
{
return (blockIndex << list._blockSizePow2BitShift) + index;
}
// We don't need Math.Min here, because if itemsToSearchCount < blockSize - itemRange.StartingItemIndex, then it would mean that
// there are no more items and we already scanned all. We want to break the loop. Subtracting a higher value from itemsToSearchCount
// will always result in a negative result.
itemsToSearchCount -= blockSize - itemRange.StartingItemIndex;
if (itemsToSearchCount <= 0)
{
return ItemNotFoundIndexLong;
}
//T itemToFind = itemRange.ItemToFind;
//int lastBlockWithData = itemRange.LastBlockWithData;
blockIndex++;
while (itemsToSearchCount > blockSize)
{
index = Array.IndexOf(list._memoryBlocks[blockIndex], itemToFind, 0, blockSize);
if (itemFoundSignal.IsSet)
{
return ItemNotFoundIndexLong;
}
else if (index >= 0)
{
return ((long)blockIndex << list._blockSizePow2BitShift) + index;
}
itemsToSearchCount -= blockSize;
blockIndex++;
}
index = itemsToSearchCount <= 0 //|| blockIndex > list.LastBlockWithData
? ItemNotFoundIndex
// : blockIndex == list._nextItemBlockIndex
// ? Array.IndexOf(list._memoryBlocks[blockIndex], itemToFind, 0, (int)itemsToSearchCount)
: Array.IndexOf(list._memoryBlocks[blockIndex], itemToFind, 0, (int)itemsToSearchCount);
// ? Array.IndexOf(memoryBlocks[blockIndex], itemToFind, 0, checked((int)Math.Min(itemRange.List._nextItemIndex, itemsToSearchCount)))
//: Array.IndexOf(memoryBlocks[blockIndex], itemToFind, 0, checked((int)Math.Min(blockSize, itemsToSearchCount)));
return !itemFoundSignal.IsSet && index >= 0 ? ((long)blockIndex << list._blockSizePow2BitShift) + index : ItemNotFoundIndexLong;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private long DoIndexOfParallel(in T item)
{
if (_longCount == 0)
{
return ItemNotFoundIndex;
}
using var context = ParallelSynchronizationContextPool.GetWithOneParticipant();
// (long)(_longCount * 0.329) => most efficient step, based on benchmarks
Iterate(context, item);
context.AllDoneSignal.SignalAndWait();
if (context.Exception == null)
{
return context.FoundItemIndex;
}
context.Exception.CaptureAndRethrow();
return ItemNotFoundIndexLong;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.NoOptimization)]
private static bool ScheduleIndexOfTask(ParallelSynchronizationContext context, RecyclableList<T> list, T itemToFind, ItemRange itemRange)
{
_ = context.AllDoneSignal.AddParticipant();
// & WAS SLOWER
// _ = ThreadPool.QueueUserWorkItem<object?>((_) =>
_ = Task.Factory.StartNew(() =>
{
try
{
if (context.ItemFoundSignal.IsSet)
{
return;
}
var index = DoLongIndexOfParallel(list, itemRange, itemToFind, context.ItemFoundSignal);
if (index >= 0)
{
context.ItemFoundSignal.Set();
_ = Interlocked.Exchange(ref context.FoundItemIndex, index);
}
}
catch (Exception e)
{
_ = Interlocked.Exchange(ref context.Exception, e);
}
finally
{
context.AllDoneSignal.RemoveParticipant();
itemRange.Dispose();
}
});
return !context.ItemFoundSignal.IsSet;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
protected static long DoIndexOf(T item, in T[][] memoryBlocks, int lastBlockIndex, int blockSize, int nextItemIndex)
{
Span<T[]> memoryBlocksSpan = new(memoryBlocks, 0, lastBlockIndex + 1);
int itemIndex, blockIndex;
for (blockIndex = 0; blockIndex < lastBlockIndex; blockIndex++)
{
itemIndex = Array.IndexOf(memoryBlocksSpan[blockIndex], item, 0, blockSize);
if (itemIndex >= 0)
{
return itemIndex + ((long)blockIndex << (31 - BitOperations.LeadingZeroCount((uint)blockSize)));
}
}
if (blockIndex == lastBlockIndex)
{
itemIndex = nextItemIndex != 0
? Array.IndexOf(memoryBlocksSpan[lastBlockIndex], item, 0, nextItemIndex)
: Array.IndexOf(memoryBlocksSpan[lastBlockIndex], item, 0, blockSize);
}
else
{
return ItemNotFoundIndexLong;
}
return itemIndex >= 0 ? itemIndex + ((long)lastBlockIndex << (31 - BitOperations.LeadingZeroCount((uint)blockSize))) : ItemNotFoundIndexLong;
} }
/// <summary> /// <summary>
@@ -482,54 +329,32 @@ namespace Recyclable.Collections
// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.NoOptimization)] // [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.NoOptimization)]
// & WAS SLOWER // & WAS SLOWER
// [MethodImpl(MethodImplOptions.NoInlining)] // [MethodImpl(MethodImplOptions.NoInlining)]
private void Iterate(in ParallelSynchronizationContext context, in T itemToFind) //private static void Iterate(RecyclableList<T> list, in ParallelSynchronizationContext context, in T itemToFind)
{ //{
// TODO: Switch to use _blockSize instead of list._longCountIndeexOfStep for simplifications & better performance. // // TODO: Switch to use _blockSize instead of list._longCountIndeexOfStep for simplifications & better performance.
// & WAS SLOWER without // // & WAS SLOWER without
ItemRange itemRange; // int blockIndex = 0;
int blockIndex = 0;
// & WAS SLOWER
// int blockSize = _blockSize;
long itemIndex = 0;
long itemsCount = _longCount;
// & WAS SLOWER
// int blockSize = _blockSize;
// long step = list._longCountIndexOfStep;
// & WAS SLOWER
// while ((blockIndex << list._blockSizePow2BitShift) + itemIndex > step)
// & WAS SLOWER
// long longCountIndexOfStep = _longCountIndexOfStep;
while (itemsCount > _longCountIndexOfStep)
{
// At this point itemIndex is limited to blockSize range - int.
itemRange = ItemRangePool.Create(blockIndex, (int)itemIndex, _longCountIndexOfStep);
if (!ScheduleIndexOfTask(context, this, itemToFind, itemRange))
{
break;
}
itemsCount -= _longCountIndexOfStep; // long itemsCount = list._longCount;
// // & WAS SLOWER
// // int blockSize = _blockSize;
// // & WAS SLOWER
// // while ((blockIndex << list._blockSizePow2BitShift) + itemIndex > step)
// while (itemsCount > list._blockSize)
// {
// // At this point itemIndex is limited to blockSize range - int.
// if (!ScheduleIndexOfTask(context, list, blockIndex, list._blockSize, itemToFind))
// {
// break;
// }
// & WAS SLOWER // itemsCount -= list._blockSize;
// blockIndex = (int)Math.DivRem(itemIndex + step, step, out itemIndex); // blockIndex++;
// & WAS SLOWER // }
// blockIndex += (int)((itemIndex + list._longCountIndexOfStep) >> list._blockSizePow2BitShift);
// itemIndex = (int)((itemIndex + list._longCountIndexOfStep) & list._blockSizeMinus1);
itemIndex += _longCountIndexOfStep; // _ = ScheduleIndexOfTask(context, list, blockIndex, (int)itemsCount, itemToFind);
while (itemIndex >= _blockSize) //}
{
blockIndex++;
itemIndex -= _blockSize;
}
}
// & WAS SLOWER
// itemRange = new(blockIndex, (int)itemIndex, list._longCount - (blockIndex << list._blockSizePow2BitShift) - itemIndex);
itemRange = ItemRangePool.Create(blockIndex, (int)itemIndex, itemsCount);
_ = ScheduleIndexOfTask(context, this, itemToFind, itemRange);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected static void SetupBlockArrayPooling(RecyclableList<T> list, int blockSize, ArrayPool<T>? blockArrayPool = null) protected static void SetupBlockArrayPooling(RecyclableList<T> list, int blockSize, ArrayPool<T>? blockArrayPool = null)
@@ -648,9 +473,12 @@ namespace Recyclable.Collections
_nextItemBlockIndex++; _nextItemBlockIndex++;
_nextItemIndex = 0; _nextItemIndex = 0;
} }
else if (_nextItemIndex == 1)
{
_lastBlockWithData++;
}
_longCount++; _longCount++;
_longCountIndexOfStep = Math.Max((long)(_longCount * OptimalParallelSearchStep), 1L);
} }
public void AddRange(in T[] items) public void AddRange(in T[] items)
@@ -693,8 +521,8 @@ namespace Recyclable.Collections
_nextItemIndex = itemsSpan.Length; _nextItemIndex = itemsSpan.Length;
_longCount = targetCapacity; _longCount = targetCapacity;
_longCountIndexOfStep = Math.Max((long)(targetCapacity * OptimalParallelSearchStep), 1L);
_nextItemBlockIndex = targetBlockIndex; _nextItemBlockIndex = targetBlockIndex;
_lastBlockWithData = targetBlockIndex - (itemsSpan.Length > 0 ? 0 : 1);
} }
public void AddRange(RecyclableArrayList<T> items) public void AddRange(RecyclableArrayList<T> items)
@@ -733,8 +561,8 @@ namespace Recyclable.Collections
_nextItemIndex = itemsSpan.Length; _nextItemIndex = itemsSpan.Length;
itemsSpan.CopyTo(targetBlockArraySpan); itemsSpan.CopyTo(targetBlockArraySpan);
_longCount = targetCapacity; _longCount = targetCapacity;
_longCountIndexOfStep = Math.Max((long)(targetCapacity * OptimalParallelSearchStep), 1L);
_nextItemBlockIndex = targetBlockIndex; _nextItemBlockIndex = targetBlockIndex;
_lastBlockWithData = targetBlockIndex - (itemsSpan.Length > 0 ? 0 : 1);
} }
public void AddRange(RecyclableList<T> items) public void AddRange(RecyclableList<T> items)
@@ -794,6 +622,7 @@ namespace Recyclable.Collections
} }
} }
_lastBlockWithData = targetBlockIndex;
if ((targetCapacity & _blockSizeMinus1) == 0) if ((targetCapacity & _blockSizeMinus1) == 0)
{ {
_nextItemIndex = 0; _nextItemIndex = 0;
@@ -806,7 +635,6 @@ namespace Recyclable.Collections
} }
_longCount = targetCapacity; _longCount = targetCapacity;
_longCountIndexOfStep = Math.Max((long)(targetCapacity * OptimalParallelSearchStep), 1L);
} }
public void AddRange(List<T> items) public void AddRange(List<T> items)
@@ -853,7 +681,7 @@ namespace Recyclable.Collections
_nextItemBlockIndex = targetBlockIndex; _nextItemBlockIndex = targetBlockIndex;
_longCount = targetCapacity; _longCount = targetCapacity;
_longCountIndexOfStep = Math.Max((long)(targetCapacity * OptimalParallelSearchStep), 1L); _lastBlockWithData = targetBlockIndex - (_nextItemIndex > 0 ? 0 : 1);
} }
public void AddRange(IList<T> items) public void AddRange(IList<T> items)
@@ -898,9 +726,9 @@ namespace Recyclable.Collections
itemsSpan.CopyTo(targetBlockArraySpan); itemsSpan.CopyTo(targetBlockArraySpan);
_longCount = targetCapacity; _longCount = targetCapacity;
_longCountIndexOfStep = Math.Max((long)(targetCapacity * OptimalParallelSearchStep), 1L);
_nextItemBlockIndex = targetBlockIndex; _nextItemBlockIndex = targetBlockIndex;
_nextItemIndex = itemsSpan.Length; _nextItemIndex = itemsSpan.Length;
_lastBlockWithData = targetBlockIndex - (itemsSpan.Length > 0 ? 0 : 1);
} }
finally finally
{ {
@@ -976,8 +804,8 @@ namespace Recyclable.Collections
_reservedBlockCount = 0; _reservedBlockCount = 0;
_nextItemBlockIndex = 0; _nextItemBlockIndex = 0;
_nextItemIndex = 0; _nextItemIndex = 0;
_lastBlockWithData = RecyclableDefaults.ItemNotFoundIndex;
_longCount = 0; _longCount = 0;
_longCountIndexOfStep = 1;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1010,17 +838,81 @@ namespace Recyclable.Collections
public IEnumerator<T> GetEnumerator() => _memoryBlocks.Enumerate(_blockSize, LongCount).GetEnumerator(); public IEnumerator<T> GetEnumerator() => _memoryBlocks.Enumerate(_blockSize, LongCount).GetEnumerator();
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int IndexOf(T item) => checked((int)DoIndexOfParallel(item)); public int IndexOf(T item)
{
if (_lastBlockWithData == 0)
{
return Array.IndexOf(_memoryBlocks[0], item, 0, _longCount < _blockSize ? _nextItemIndex : _blockSize);
}
else if (_longCount == 0)
{
return RecyclableDefaults.ItemNotFoundIndex;
}
int itemIndex = Array.IndexOf(_memoryBlocks[0], item, 0, _longCount < _blockSize ? _nextItemIndex : _blockSize);
if (itemIndex >= 0)
{
return itemIndex;
}
else if (_longCount < RecyclableDefaults.MinItemsCountForParallelization)
{
return (int)RecyclableListIndexOfHelpers.IndexOfSequential(this, item);
}
else
{
// & WAS SLOWER
// long toSearch = _longCount - _blockSize;
if (_longCount > _blockSize)
{
itemIndex = Array.IndexOf(_memoryBlocks[1], item, 0, (int)Math.Min(_blockSize, _longCount - _blockSize));
return itemIndex >= 0 ? _blockSize + itemIndex : (int)RecyclableListIndexOfHelpers.IndexOfParallel(this, item);
}
else
{
return RecyclableDefaults.ItemNotFoundIndex;
}
}
}
public void Insert(int index, T item) => throw new NotSupportedException(); public void Insert(int index, T item) => throw new NotSupportedException();
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public long LongIndexOf(T item) => _longCount == 0 public long LongIndexOf(T item)
? ItemNotFoundIndexLong {
: _longCount <= _blockSize if (_lastBlockWithData == 0)
? Array.IndexOf(_memoryBlocks[0], item, 0, checked((int)_longCount)) {
: DoIndexOfParallel(item); return Array.IndexOf(_memoryBlocks[0], item, 0, _longCount < _blockSize ? _nextItemIndex : _blockSize);
}
else if (_longCount == 0)
{
return RecyclableDefaults.ItemNotFoundIndex;
}
int itemIndex = Array.IndexOf(_memoryBlocks[0], item, 0, _longCount < _blockSize ? _nextItemIndex : _blockSize);
if (itemIndex >= 0)
{
return itemIndex;
}
else if (_longCount < RecyclableDefaults.MinItemsCountForParallelization)
{
return RecyclableListIndexOfHelpers.IndexOfSequential(this, item);
}
else
{
// & WAS SLOWER
// long toSearch = _longCount - _blockSize;
if (_longCount > _blockSize)
{
itemIndex = Array.IndexOf(_memoryBlocks[1], item, 0, (int)Math.Min(_blockSize, _longCount - _blockSize));
return itemIndex >= 0 ? _blockSize + itemIndex : RecyclableListIndexOfHelpers.IndexOfParallel(this, item);
}
else
{
return RecyclableDefaults.ItemNotFoundIndex;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool Remove(T item) public bool Remove(T item)
@@ -1029,7 +921,6 @@ namespace Recyclable.Collections
if (index >= 0) if (index >= 0)
{ {
_longCount--; _longCount--;
_longCountIndexOfStep = Math.Max((long)(_longCount * OptimalParallelSearchStep), 1L);
if (index < _longCount) if (index < _longCount)
{ {
@@ -1039,6 +930,10 @@ namespace Recyclable.Collections
if (_nextItemIndex > 0) if (_nextItemIndex > 0)
{ {
_nextItemIndex--; _nextItemIndex--;
if (_nextItemIndex == 0)
{
_lastBlockWithData--;
}
} }
else else
{ {
@@ -1080,6 +975,8 @@ namespace Recyclable.Collections
{ {
_nextItemBlockIndex--; _nextItemBlockIndex--;
} }
_lastBlockWithData--;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
@@ -1091,7 +988,6 @@ namespace Recyclable.Collections
} }
_longCount--; _longCount--;
_longCountIndexOfStep = Math.Max((long)(_longCount * OptimalParallelSearchStep), 1L);
if (index < _longCount) if (index < _longCount)
{ {
@@ -1101,6 +997,10 @@ namespace Recyclable.Collections
if (_nextItemIndex > 0) if (_nextItemIndex > 0)
{ {
_nextItemIndex--; _nextItemIndex--;
if (_nextItemIndex == 0)
{
_lastBlockWithData--;
}
} }
else else
{ {
@@ -1125,7 +1025,6 @@ namespace Recyclable.Collections
} }
_longCount--; _longCount--;
_longCountIndexOfStep = Math.Max((long)Math.Floor(_longCount * OptimalParallelSearchStep), 1L);
if (index < _longCount) if (index < _longCount)
{ {
@@ -1135,6 +1034,10 @@ namespace Recyclable.Collections
if (_nextItemIndex > 0) if (_nextItemIndex > 0)
{ {
_nextItemIndex--; _nextItemIndex--;
if (_nextItemIndex == 0)
{
_lastBlockWithData--;
}
} }
else else
{ {
@@ -8,7 +8,7 @@ namespace Recyclable.Collections
public byte BlockSizePow2BitShift { get; init; } public byte BlockSizePow2BitShift { get; init; }
public long ItemsCount { get; init; } public long ItemsCount { get; init; }
public int StartingBlockIndex { get; init; } public int StartingBlockIndex { get; init; }
public long Step { get; init; } public long BlockSize { get; init; }
// public SearchConfiguration(TList list, in TItem itemToFind) // public SearchConfiguration(TList list, in TItem itemToFind)
// { // {
@@ -23,7 +23,7 @@ namespace Recyclable.Collections
BlockSizeMinus1 = list._blockSizeMinus1, BlockSizeMinus1 = list._blockSizeMinus1,
BlockSizePow2BitShift = list._blockSizePow2BitShift, BlockSizePow2BitShift = list._blockSizePow2BitShift,
ItemsCount = list._longCount, ItemsCount = list._longCount,
Step = list._longCountIndexOfStep BlockSize = list._blockSize
}; };
} }
} }
@@ -9,7 +9,7 @@ namespace Recyclable.CollectionsTests
{ {
[Theory] [Theory]
[MemberData(nameof(TestCases))] [MemberData(nameof(TestCases))]
public void IterateShouldYieldAllItems(int startingBlockIndex, int blockSize, long itemsCount, int step, IEnumerable<(int BlockIndex, int StartingItemIndex, long ItemsCount)> expected) public void IterateShouldYieldAllItems(int startingBlockIndex, int blockSize, long itemsCount, int step, IEnumerable<(int BlockIndex, long ItemsCount)> expected)
{ {
// Prepare // Prepare
var mockList = new RecyclableList<long>(); var mockList = new RecyclableList<long>();
@@ -23,15 +23,15 @@ namespace Recyclable.CollectionsTests
BlockSizeMinus1 = blockSize - 1, BlockSizeMinus1 = blockSize - 1,
BlockSizePow2BitShift = blockSizePow2BitShift, BlockSizePow2BitShift = blockSizePow2BitShift,
ItemsCount = itemsCount, ItemsCount = itemsCount,
Step = step, BlockSize = step,
}; };
var actualItemRanges = new List<(int BlockIndex, int StartingItemIndex, long ItemsCount)>(); var actualItemRanges = new List<(int BlockIndex, long ItemsCount)>();
// Act // Act
ItemRangesIterator.Iterate(synchronizationContext, search, (context, search, itemRange) => ItemRangesIterator.Iterate(synchronizationContext, search, (context, search, itemRange) =>
{ {
actualItemRanges.Add((itemRange.BlockIndex, itemRange.StartingItemIndex, itemRange.ItemsToSearchCount)); actualItemRanges.Add((itemRange.BlockIndex, itemRange.ItemsToSearchCount));
return true; return true;
}); });
@@ -43,102 +43,102 @@ namespace Recyclable.CollectionsTests
{ {
new object[] new object[]
{ {
0, 128, 333, (long)(333 * 0.329), new(int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 128, 333, (long)(333 * 0.329), new(int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 109), (0, 109),
(0, 109, 109), (0, 109),
(1, 90, 109), (1, 109),
(2, 71, 6) (2, 6)
} }
}, },
new object[] new object[]
{ {
0, 2, 10, 2, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 2, 10, 2, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 2), (0, 2),
(1, 0, 2), (1, 2),
(2, 0, 2), (2, 2),
(3, 0, 2), (3, 2),
(4, 0, 2), (4, 2),
} }
}, },
new object[] new object[]
{ {
0, 2, 10, 3, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 2, 10, 3, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 3), (0, 3),
(1, 1, 3), (1, 3),
(3, 0, 3), (3, 3),
(4, 1, 1), (4, 1),
} }
}, },
new object[] new object[]
{ {
0, 1, 10, 3, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 1, 10, 3, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 3), (0, 3),
(3, 0, 3), (3, 3),
(6, 0, 3), (6, 3),
(9, 0, 1), (9, 1),
} }
}, },
new object[] new object[]
{ {
0, 2, 10, 4, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 2, 10, 4, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 4), (0, 4),
(2, 0, 4), (2, 4),
(4, 0, 2), (4, 2),
} }
}, },
new object[] new object[]
{ {
0, 1, 10, 2, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 1, 10, 2, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 2), (0, 2),
(2, 0, 2), (2, 2),
(4, 0, 2), (4, 2),
(6, 0, 2), (6, 2),
(8, 0, 2), (8, 2),
} }
}, },
new object[] new object[]
{ {
0, 2, 10, 1, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 2, 10, 1, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 1), (0, 1),
(0, 1, 1), (0, 1),
(1, 0, 1), (1, 1),
(1, 1, 1), (1, 1),
(2, 0, 1), (2, 1),
(2, 1, 1), (2, 1),
(3, 0, 1), (3, 1),
(3, 1, 1), (3, 1),
(4, 0, 1), (4, 1),
(4, 1, 1), (4, 1),
} }
}, },
new object[] new object[]
{ {
0, 1, 10, 1, new (int BlockIndex, int StartingItemIndex, long ItemsCount)[] 0, 1, 10, 1, new (int BlockIndex, long ItemsCount)[]
{ {
(0, 0, 1), (0, 1),
(1, 0, 1), (1, 1),
(2, 0, 1), (2, 1),
(3, 0, 1), (3, 1),
(4, 0, 1), (4, 1),
(5, 0, 1), (5, 1),
(6, 0, 1), (6, 1),
(7, 0, 1), (7, 1),
(8, 0, 1), (8, 1),
(9, 0, 1), (9, 1),
} }
}, },
}; };