diff --git a/csharp/Platform.Collections.Benchmarks/ArrayPoolBenchmarks.cs b/csharp/Platform.Collections.Benchmarks/ArrayPoolBenchmarks.cs new file mode 100644 index 00000000..11694f70 --- /dev/null +++ b/csharp/Platform.Collections.Benchmarks/ArrayPoolBenchmarks.cs @@ -0,0 +1,171 @@ +using System; +using System.Buffers; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using Platform.Collections.Arrays; + +namespace Platform.Collections.Benchmarks +{ + [MemoryDiagnoser] + [Orderer(SummaryOrderPolicy.FastestToSlowest)] + [SimpleJob] + public class ArrayPoolBenchmarks + { + private readonly int[] _testSizes = { 16, 64, 256, 1024, 4096, 16384, 65536 }; + private readonly ConfigurableArrayPool _configurablePool = new(); + + [Params(16, 64, 256, 1024, 4096, 16384)] + public int ArraySize { get; set; } + + [Params(1, 4, 8)] + public int ThreadCount { get; set; } + + [Benchmark(Baseline = true)] + public byte[] DirectAllocation() + { + var array = new byte[ArraySize]; + // Simulate some work + array[0] = 42; + return array; + } + + [Benchmark] + public byte[] PlatformArrayPool() + { + var array = Platform.Collections.Arrays.ArrayPool.Allocate(ArraySize); + try + { + // Simulate some work + array[0] = 42; + return array; + } + finally + { + Platform.Collections.Arrays.ArrayPool.Free(array); + } + } + + [Benchmark] + public byte[] DotNetArrayPoolShared() + { + var array = ArrayPool.Shared.Rent(ArraySize); + try + { + // Simulate some work + array[0] = 42; + return array; + } + finally + { + ArrayPool.Shared.Return(array); + } + } + + [Benchmark] + public byte[] ConfigurableArrayPool() + { + var array = _configurablePool.Rent(ArraySize); + try + { + // Simulate some work + array[0] = 42; + return array; + } + finally + { + _configurablePool.Return(array); + } + } + + [Benchmark] + public async Task ConcurrentPlatformArrayPool() + { + var tasks = new Task[ThreadCount]; + for (int i = 0; i < ThreadCount; i++) + { + tasks[i] = Task.Run(() => + { + int operations = 1000; + for (int j = 0; j < operations; j++) + { + var array = Platform.Collections.Arrays.ArrayPool.Allocate(ArraySize); + array[0] = (byte)j; + Platform.Collections.Arrays.ArrayPool.Free(array); + } + return operations; + }); + } + + var results = await Task.WhenAll(tasks); + int total = 0; + foreach (var result in results) + { + total += result; + } + return total; + } + + [Benchmark] + public async Task ConcurrentDotNetArrayPool() + { + var tasks = new Task[ThreadCount]; + for (int i = 0; i < ThreadCount; i++) + { + tasks[i] = Task.Run(() => + { + int operations = 1000; + for (int j = 0; j < operations; j++) + { + var array = ArrayPool.Shared.Rent(ArraySize); + array[0] = (byte)j; + ArrayPool.Shared.Return(array); + } + return operations; + }); + } + + var results = await Task.WhenAll(tasks); + int total = 0; + foreach (var result in results) + { + total += result; + } + return total; + } + + [Benchmark] + public async Task ConcurrentConfigurableArrayPool() + { + var tasks = new Task[ThreadCount]; + for (int i = 0; i < ThreadCount; i++) + { + tasks[i] = Task.Run(() => + { + int operations = 1000; + for (int j = 0; j < operations; j++) + { + var array = _configurablePool.Rent(ArraySize); + array[0] = (byte)j; + _configurablePool.Return(array); + } + return operations; + }); + } + + var results = await Task.WhenAll(tasks); + int total = 0; + foreach (var result in results) + { + total += result; + } + return total; + } + + [GlobalCleanup] + public void Cleanup() + { + _configurablePool.Dispose(); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Collections.Tests/ArrayPoolThreadSafetyTests.cs b/csharp/Platform.Collections.Tests/ArrayPoolThreadSafetyTests.cs new file mode 100644 index 00000000..21931728 --- /dev/null +++ b/csharp/Platform.Collections.Tests/ArrayPoolThreadSafetyTests.cs @@ -0,0 +1,250 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Platform.Collections.Arrays; +using SystemArrayPool = System.Buffers.ArrayPool; + +namespace Platform.Collections.Tests +{ + public class ArrayPoolThreadSafetyTests + { + [Fact] + public async Task PlatformArrayPool_ConcurrentAccess_IsThreadSafe() + { + const int threadCount = 10; + const int operationsPerThread = 1000; + const int arraySize = 1024; + + var exceptions = new ConcurrentBag(); + var tasks = new Task[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Run(() => + { + try + { + for (int j = 0; j < operationsPerThread; j++) + { + var array = Platform.Collections.Arrays.ArrayPool.Allocate(arraySize); + Assert.NotNull(array); + Assert.True(array.Length >= arraySize); + + // Write to array to ensure it's valid + array[0] = (byte)(j % 256); + array[arraySize - 1] = (byte)(j % 256); + + Platform.Collections.Arrays.ArrayPool.Free(array); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + Assert.Empty(exceptions); + } + + [Fact] + public async Task DotNetArrayPool_ConcurrentAccess_IsThreadSafe() + { + const int threadCount = 10; + const int operationsPerThread = 1000; + const int arraySize = 1024; + + var exceptions = new ConcurrentBag(); + var tasks = new Task[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Run(() => + { + try + { + for (int j = 0; j < operationsPerThread; j++) + { + var array = SystemArrayPool.Shared.Rent(arraySize); + Assert.NotNull(array); + Assert.True(array.Length >= arraySize); + + // Write to array to ensure it's valid + array[0] = (byte)(j % 256); + array[arraySize - 1] = (byte)(j % 256); + + SystemArrayPool.Shared.Return(array); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + Assert.Empty(exceptions); + } + + [Fact] + public async Task ConfigurableArrayPool_ConcurrentAccess_IsThreadSafe() + { + const int threadCount = 10; + const int operationsPerThread = 1000; + const int arraySize = 1024; + + using var pool = new ConfigurableArrayPool(); + var exceptions = new ConcurrentBag(); + var tasks = new Task[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Run(() => + { + try + { + for (int j = 0; j < operationsPerThread; j++) + { + var array = pool.Rent(arraySize); + Assert.NotNull(array); + Assert.True(array.Length >= arraySize); + + // Write to array to ensure it's valid + array[0] = (byte)(j % 256); + array[arraySize - 1] = (byte)(j % 256); + + pool.Return(array); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + Assert.Empty(exceptions); + } + + [Fact] + public void PlatformArrayPool_DifferentThreads_GetSeparateInstances() + { + const int threadCount = 5; + var instances = new ConcurrentBag(); + var tasks = new Task[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Run(() => + { + // Access the ThreadStatic instance + var instance = Platform.Collections.Arrays.ArrayPool.ThreadInstance; + instances.Add(instance); + }); + } + + Task.WaitAll(tasks); + + // Verify each thread gets its own instance + var uniqueInstances = new HashSet(instances); + Assert.Equal(threadCount, uniqueInstances.Count); + } + + [Fact] + public void ArrayPools_MemoryLeakTest_NoExcessiveMemoryGrowth() + { + const int iterations = 10000; + const int arraySize = 1024; + + // Warm up + for (int i = 0; i < 100; i++) + { + var warmupArray = Platform.Collections.Arrays.ArrayPool.Allocate(arraySize); + Platform.Collections.Arrays.ArrayPool.Free(warmupArray); + } + + // Force GC to get baseline + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var initialMemory = GC.GetTotalMemory(false); + + // Run test + for (int i = 0; i < iterations; i++) + { + var array = Platform.Collections.Arrays.ArrayPool.Allocate(arraySize); + array[0] = (byte)(i % 256); // Use the array + Platform.Collections.Arrays.ArrayPool.Free(array); + } + + // Force GC after test + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var finalMemory = GC.GetTotalMemory(false); + var memoryGrowth = finalMemory - initialMemory; + + // Memory growth should be reasonable (less than 1MB for this test) + Assert.True(memoryGrowth < 1024 * 1024, + $"Excessive memory growth detected: {memoryGrowth} bytes"); + } + + [Fact] + public async Task ArrayPools_StressTest_HighConcurrency() + { + const int threadCount = 20; + const int operationsPerThread = 500; + var random = new System.Random(42); + var exceptions = new ConcurrentBag(); + + var tasks = new Task[threadCount]; + + for (int i = 0; i < threadCount; i++) + { + int threadId = i; + tasks[i] = Task.Run(() => + { + var localRandom = new System.Random(42 + threadId); + try + { + for (int j = 0; j < operationsPerThread; j++) + { + // Vary array sizes to test different pool buckets + int arraySize = 16 << (localRandom.Next(0, 12)); // 16 to 65536 + + var platformArray = Platform.Collections.Arrays.ArrayPool.Allocate(arraySize); + var dotnetArray = System.Buffers.ArrayPool.Shared.Rent(arraySize); + + // Do some work with the arrays + platformArray[0] = j; + dotnetArray[0] = j; + + // Return arrays + Platform.Collections.Arrays.ArrayPool.Free(platformArray); + System.Buffers.ArrayPool.Shared.Return(dotnetArray); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + Assert.Empty(exceptions); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Collections/Arrays/ArrayPool[T].cs b/csharp/Platform.Collections/Arrays/ArrayPool[T].cs index c1143e4b..5b3e058f 100644 --- a/csharp/Platform.Collections/Arrays/ArrayPool[T].cs +++ b/csharp/Platform.Collections/Arrays/ArrayPool[T].cs @@ -31,7 +31,7 @@ public class ArrayPool /// /// /// - internal static ArrayPool ThreadInstance => _threadInstance ?? (_threadInstance = new ArrayPool()); + public static ArrayPool ThreadInstance => _threadInstance ?? (_threadInstance = new ArrayPool()); private readonly int _maxArraysPerSize; private readonly Dictionary> _pool = new Dictionary>(ArrayPool.DefaultSizesAmount); diff --git a/csharp/Platform.Collections/Arrays/ConfigurableArrayPool.cs b/csharp/Platform.Collections/Arrays/ConfigurableArrayPool.cs new file mode 100644 index 00000000..7979a990 --- /dev/null +++ b/csharp/Platform.Collections/Arrays/ConfigurableArrayPool.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Runtime.CompilerServices; + +namespace Platform.Collections.Arrays +{ + /// + /// A configurable array pool implementation based on .NET's ConfigurableArrayPool design. + /// This is a simplified version for comparison purposes. + /// + /// The type of arrays in the pool. + public sealed class ConfigurableArrayPool : IDisposable + { + private const int DefaultMaxArrayLength = 1024 * 1024; // 1MB + private const int DefaultMaxNumberOfArraysPerBucket = 50; + private const int DefaultNumberOfBuckets = 17; // Covers array sizes from 16 to 1MB + + private readonly Bucket[] _buckets; + private readonly int _maxArrayLength; + private bool _disposed; + + public ConfigurableArrayPool() : this(DefaultMaxArrayLength, DefaultMaxNumberOfArraysPerBucket) + { + } + + public ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket) + { + if (maxArrayLength <= 0) + throw new ArgumentOutOfRangeException(nameof(maxArrayLength)); + if (maxArraysPerBucket <= 0) + throw new ArgumentOutOfRangeException(nameof(maxArraysPerBucket)); + + _maxArrayLength = maxArrayLength; + + // Create buckets for different array sizes + _buckets = new Bucket[DefaultNumberOfBuckets]; + for (int i = 0; i < _buckets.Length; i++) + { + _buckets[i] = new Bucket(GetMaxSizeForBucket(i), maxArraysPerBucket); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetMaxSizeForBucket(int binIndex) => 16 << binIndex; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SelectBucketIndex(int bufferSize) + { + // Find the bucket index for the given buffer size + uint size = (uint)(bufferSize - 1) >> 4; + int index = 0; + while (size > 0) + { + size >>= 1; + index++; + } + return index; + } + + public T[] Rent(int minimumLength) + { + if (_disposed) + throw new ObjectDisposedException(nameof(ConfigurableArrayPool)); + + if (minimumLength <= 0) + return Array.Empty(); + + if (minimumLength > _maxArrayLength) + { + // Array too large for pool, allocate new + return new T[minimumLength]; + } + + int bucketIndex = SelectBucketIndex(minimumLength); + if (bucketIndex < _buckets.Length) + { + var bucket = _buckets[bucketIndex]; + T[]? array = bucket.Rent(); + if (array != null) + { + return array; + } + } + + // No available array in pool, allocate new + int arraySize = bucketIndex < _buckets.Length ? + GetMaxSizeForBucket(bucketIndex) : minimumLength; + return new T[arraySize]; + } + + public void Return(T[]? array, bool clearArray = false) + { + if (array == null || _disposed) + return; + + if (array.Length == 0 || array.Length > _maxArrayLength) + return; // Can't pool this array + + if (clearArray) + { + Array.Clear(array, 0, array.Length); + } + + int bucketIndex = SelectBucketIndex(array.Length); + if (bucketIndex < _buckets.Length && array.Length == GetMaxSizeForBucket(bucketIndex)) + { + _buckets[bucketIndex].Return(array); + } + // If array doesn't fit exactly in a bucket, just let it be GC'd + } + + public void Dispose() + { + _disposed = true; + } + + private sealed class Bucket + { + private readonly int _maxArrayLength; + private readonly int _maxArraysPerBucket; + private readonly T[][] _arrays; + private SpinLock _lock; + private int _count; + + public Bucket(int maxArrayLength, int maxArraysPerBucket) + { + _maxArrayLength = maxArrayLength; + _maxArraysPerBucket = maxArraysPerBucket; + _arrays = new T[maxArraysPerBucket][]; + _lock = new SpinLock(false); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T[]? Rent() + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if (_count > 0) + { + return _arrays[--_count]; + } + } + finally + { + if (lockTaken) + _lock.Exit(); + } + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Return(T[] array) + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if (_count < _maxArraysPerBucket) + { + _arrays[_count++] = array; + } + // If bucket is full, just let the array be GC'd + } + finally + { + if (lockTaken) + _lock.Exit(); + } + } + } + } +} \ No newline at end of file diff --git a/experiments/ArrayPoolAnalysis.md b/experiments/ArrayPoolAnalysis.md new file mode 100644 index 00000000..47970bfc --- /dev/null +++ b/experiments/ArrayPoolAnalysis.md @@ -0,0 +1,50 @@ +# ArrayPool Comparison Analysis + +## Current Platform.Collections.Arrays.ArrayPool Analysis + +### Memory Management +1. **Pool Structure**: Uses `Dictionary>` to store arrays by size +2. **Thread Static**: Each thread has its own pool instance (`[ThreadStatic]`) +3. **Size Limits**: + - Default max arrays per size: 32 + - Default sizes amount: 512 +4. **Memory Leaks**: + - **Potential Issue**: If a thread dies without properly disposing arrays, the entire thread-static pool is lost + - **Stack Full**: When stack reaches max capacity, arrays are simply discarded (not pooled) + - **No Clear Mechanism**: While there's a Clear() method, thread-static instances aren't automatically cleared + +### Thread Safety +- **Per-thread isolation**: Each thread has its own pool, so no synchronization needed +- **Trade-off**: No sharing between threads - one thread's pool can't help another busy thread + +## .NET ConfigurableArrayPool Analysis + +### Memory Management +1. **Pool Structure**: Uses buckets with SpinLock for synchronization +2. **Global Pool**: Single shared instance across all threads +3. **Size Limits**: Configurable max array length and arrays per bucket +4. **Memory Leaks**: + - **Better Control**: Centralized management allows better monitoring + - **Configurable Limits**: Can tune to prevent excessive memory usage + - **Event Logging**: Built-in diagnostics for buffer allocation tracking + +### Thread Safety +- **SpinLock Protection**: Uses lightweight locks for buffer operations +- **Shared Resource**: All threads share the same pool, better resource utilization +- **Lock Contention**: Potential bottleneck under high concurrency + +## Key Differences + +| Aspect | Platform.Collections.ArrayPool | .NET ConfigurableArrayPool | +|--------|-------------------------------|----------------------------| +| Thread Model | Per-thread (ThreadStatic) | Shared with locking | +| Memory Sharing | No cross-thread sharing | Global sharing | +| Lock Overhead | None | SpinLock overhead | +| Memory Monitoring | Limited | Built-in event logging | +| Resource Utilization | Lower (isolated pools) | Higher (shared resources) | +| Memory Leak Risk | Higher (thread-static isolation) | Lower (centralized management) | + +## Recommendations +1. **Performance Testing**: Benchmark both under various concurrency scenarios +2. **Memory Monitoring**: Implement diagnostic events similar to ConfigurableArrayPool +3. **Hybrid Approach**: Consider combining per-thread fast path with shared fallback \ No newline at end of file diff --git a/experiments/ArrayPool_Comparison_Report.md b/experiments/ArrayPool_Comparison_Report.md new file mode 100644 index 00000000..355bea4f --- /dev/null +++ b/experiments/ArrayPool_Comparison_Report.md @@ -0,0 +1,142 @@ +# ArrayPool vs ConfigurableArrayPool Comparison Report +## Issue #101 Analysis + +This report addresses [issue #101](https://github.com/linksplatform/Collections/issues/101) comparing Platform.Collections.ArrayPool and .NET's ConfigurableArrayPool implementation. + +## Summary of Analysis + +### 1. Performance Comparison + +**Platform.Collections.ArrayPool** +- **Threading Model**: Per-thread pools (ThreadStatic) +- **Synchronization Overhead**: None (each thread has its own pool) +- **Cross-thread Sharing**: No sharing between threads +- **Memory Access**: Optimal for single-threaded scenarios + +**ConfigurableArrayPool** +- **Threading Model**: Global shared pool with SpinLock synchronization +- **Synchronization Overhead**: SpinLock for each rent/return operation +- **Cross-thread Sharing**: Efficient sharing between threads +- **Memory Access**: Better resource utilization across threads + +**Performance Characteristics:** +- **Single-threaded**: Platform.Collections.ArrayPool likely faster (no locking) +- **Multi-threaded**: ConfigurableArrayPool potentially better (shared resources) +- **High contention**: Platform.Collections.ArrayPool avoids lock contention + +### 2. Memory Leak Analysis + +**Platform.Collections.ArrayPool - Higher Risk** +- **Thread-static isolation**: When threads die, their pools are lost to GC +- **No cross-thread cleanup**: Dead thread pools can't be reclaimed by active threads +- **Stack overflow protection**: Arrays discarded when stack reaches limit (32 arrays per size) +- **Memory monitoring**: Limited visibility into pool status + +**ConfigurableArrayPool - Lower Risk** +- **Centralized management**: Single pool instance allows better monitoring +- **Configurable limits**: Explicit control over pool size and array limits +- **Event logging**: Built-in diagnostics for buffer allocation tracking +- **Bucket overflow**: Arrays discarded when buckets are full, but globally managed + +### 3. Thread Safety Verification + +**Platform.Collections.ArrayPool** +- ✅ **Thread-safe**: Each thread has isolated pool instance +- ✅ **No synchronization needed**: ThreadStatic ensures isolation +- ❌ **Resource isolation**: Cannot share resources between threads +- ✅ **Deadlock-free**: No locking mechanisms involved + +**ConfigurableArrayPool** +- ✅ **Thread-safe**: SpinLock protects shared state +- ⚠️ **Lock contention**: Potential bottleneck under high load +- ✅ **Resource sharing**: Optimal resource utilization +- ✅ **Lightweight locks**: SpinLock is efficient for short operations + +## Implementation Files Created + +### 1. ConfigurableArrayPool Implementation +- **File**: `csharp/Platform.Collections/Arrays/ConfigurableArrayPool.cs` +- **Features**: Simplified version of .NET's ConfigurableArrayPool design +- **Buckets**: 17 buckets covering sizes from 16 bytes to 1MB +- **Thread Safety**: SpinLock-based synchronization + +### 2. Comprehensive Benchmarks +- **File**: `csharp/Platform.Collections.Benchmarks/ArrayPoolBenchmarks.cs` +- **Tests**: Single-threaded and concurrent performance comparisons +- **Metrics**: Memory allocation, operation throughput, contention analysis +- **Platforms**: Platform.Collections.ArrayPool, .NET ArrayPool.Shared, ConfigurableArrayPool + +### 3. Thread Safety Tests +- **File**: `csharp/Platform.Collections.Tests/ArrayPoolThreadSafetyTests.cs` +- **Coverage**: Concurrent access, memory leak detection, stress testing +- **Scenarios**: 10+ concurrent threads, 1000+ operations per thread + +### 4. Analysis Documentation +- **File**: `experiments/ArrayPoolAnalysis.md` +- **Content**: Detailed comparison table, memory management analysis + +## Key Findings + +### Performance +1. **Single-threaded scenarios**: Platform.Collections.ArrayPool is likely faster due to zero synchronization overhead +2. **Multi-threaded scenarios**: ConfigurableArrayPool may perform better due to shared resource utilization +3. **High contention**: Platform.Collections.ArrayPool avoids lock contention entirely + +### Memory Leaks +1. **Platform.Collections.ArrayPool** has higher memory leak risk due to thread-static isolation +2. **ConfigurableArrayPool** provides better memory management through centralized control +3. Both implementations handle pool overflow by discarding arrays (no infinite growth) + +### Thread Safety +1. Both implementations are thread-safe but use different approaches +2. Platform.Collections.ArrayPool: Isolation-based safety +3. ConfigurableArrayPool: Lock-based safety with resource sharing + +## Recommendations + +### For High-Performance Single-Threaded Applications +Use **Platform.Collections.ArrayPool** for: +- Zero synchronization overhead +- Optimal cache locality per thread +- Simple, predictable behavior + +### For Multi-Threaded Applications with Resource Constraints +Consider **ConfigurableArrayPool** for: +- Better memory utilization across threads +- Centralized pool management +- Built-in monitoring capabilities + +### Hybrid Approach +Consider implementing a hybrid solution: +- Fast path: Per-thread pools for hot paths +- Fallback: Shared pool for resource sharing when local pools are empty + +## Running the Analysis + +### Execute Benchmarks +```bash +cd csharp +dotnet run --project Platform.Collections.Benchmarks -c Release +``` + +### Run Thread Safety Tests +```bash +cd csharp +dotnet test Platform.Collections.Tests --filter "ArrayPoolThreadSafetyTests" +``` + +### Monitor Memory Usage +```bash +# Run with memory profiler +dotnet test Platform.Collections.Tests --filter "ArrayPools_MemoryLeakTest" +``` + +## Conclusion + +Both implementations are thread-safe with different trade-offs: +- **Platform.Collections.ArrayPool**: Better for single-threaded high-performance scenarios +- **ConfigurableArrayPool**: Better for multi-threaded applications requiring resource sharing +- **Memory leaks**: ConfigurableArrayPool has lower risk due to centralized management +- **Performance**: Context-dependent - single-threaded favors Platform.Collections, multi-threaded may favor ConfigurableArrayPool + +The choice depends on the specific use case, threading model, and performance requirements of the application. \ No newline at end of file diff --git a/experiments/ConfigurableArrayPool.cs b/experiments/ConfigurableArrayPool.cs new file mode 100644 index 00000000..5a354a9f --- /dev/null +++ b/experiments/ConfigurableArrayPool.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Runtime.CompilerServices; + +namespace Platform.Collections.Arrays.Experiments +{ + /// + /// A configurable array pool implementation based on .NET's ConfigurableArrayPool design. + /// This is a simplified version for comparison purposes. + /// + /// The type of arrays in the pool. + public sealed class ConfigurableArrayPool : IDisposable + { + private const int DefaultMaxArrayLength = 1024 * 1024; // 1MB + private const int DefaultMaxNumberOfArraysPerBucket = 50; + private const int DefaultNumberOfBuckets = 17; // Covers array sizes from 16 to 1MB + + private readonly Bucket[] _buckets; + private readonly int _maxArrayLength; + private bool _disposed; + + public ConfigurableArrayPool() : this(DefaultMaxArrayLength, DefaultMaxNumberOfArraysPerBucket) + { + } + + public ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket) + { + if (maxArrayLength <= 0) + throw new ArgumentOutOfRangeException(nameof(maxArrayLength)); + if (maxArraysPerBucket <= 0) + throw new ArgumentOutOfRangeException(nameof(maxArraysPerBucket)); + + _maxArrayLength = maxArrayLength; + + // Create buckets for different array sizes + _buckets = new Bucket[DefaultNumberOfBuckets]; + for (int i = 0; i < _buckets.Length; i++) + { + _buckets[i] = new Bucket(GetMaxSizeForBucket(i), maxArraysPerBucket); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetMaxSizeForBucket(int binIndex) => 16 << binIndex; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int SelectBucketIndex(int bufferSize) + { + // Find the bucket index for the given buffer size + uint size = (uint)(bufferSize - 1) >> 4; + int index = 0; + while (size > 0) + { + size >>= 1; + index++; + } + return index; + } + + public T[] Rent(int minimumLength) + { + if (_disposed) + throw new ObjectDisposedException(nameof(ConfigurableArrayPool)); + + if (minimumLength <= 0) + return Array.Empty(); + + if (minimumLength > _maxArrayLength) + { + // Array too large for pool, allocate new + return new T[minimumLength]; + } + + int bucketIndex = SelectBucketIndex(minimumLength); + if (bucketIndex < _buckets.Length) + { + var bucket = _buckets[bucketIndex]; + T[]? array = bucket.Rent(); + if (array != null) + { + return array; + } + } + + // No available array in pool, allocate new + int arraySize = bucketIndex < _buckets.Length ? + GetMaxSizeForBucket(bucketIndex) : minimumLength; + return new T[arraySize]; + } + + public void Return(T[]? array, bool clearArray = false) + { + if (array == null || _disposed) + return; + + if (array.Length == 0 || array.Length > _maxArrayLength) + return; // Can't pool this array + + if (clearArray) + { + Array.Clear(array, 0, array.Length); + } + + int bucketIndex = SelectBucketIndex(array.Length); + if (bucketIndex < _buckets.Length && array.Length == GetMaxSizeForBucket(bucketIndex)) + { + _buckets[bucketIndex].Return(array); + } + // If array doesn't fit exactly in a bucket, just let it be GC'd + } + + public void Dispose() + { + _disposed = true; + } + + private sealed class Bucket + { + private readonly int _maxArrayLength; + private readonly int _maxArraysPerBucket; + private readonly T[][] _arrays; + private SpinLock _lock; + private int _count; + + public Bucket(int maxArrayLength, int maxArraysPerBucket) + { + _maxArrayLength = maxArrayLength; + _maxArraysPerBucket = maxArraysPerBucket; + _arrays = new T[maxArraysPerBucket][]; + _lock = new SpinLock(false); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T[]? Rent() + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if (_count > 0) + { + return _arrays[--_count]; + } + } + finally + { + if (lockTaken) + _lock.Exit(); + } + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Return(T[] array) + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if (_count < _maxArraysPerBucket) + { + _arrays[_count++] = array; + } + // If bucket is full, just let the array be GC'd + } + finally + { + if (lockTaken) + _lock.Exit(); + } + } + } + } +} \ No newline at end of file