// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Diagnostics; using System.Threading; namespace System.Buffers { internal sealed partial class ConfigurableArrayPool : ArrayPool { /// The default maximum length of each array in the pool (2^20). private const int DefaultMaxArrayLength = 1024 * 1024; /// The default maximum number of arrays per bucket that are available for rent. private const int DefaultMaxNumberOfArraysPerBucket = 50; private readonly Bucket[] _buckets; internal ConfigurableArrayPool() : this(DefaultMaxArrayLength, DefaultMaxNumberOfArraysPerBucket) { } internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket) { if (maxArrayLength <= 0) { throw new ArgumentOutOfRangeException(nameof(maxArrayLength)); } if (maxArraysPerBucket <= 0) { throw new ArgumentOutOfRangeException(nameof(maxArraysPerBucket)); } // Our bucketing algorithm has a min length of 2^4 and a max length of 2^30. // Constrain the actual max used to those values. const int MinimumArrayLength = 0x10, MaximumArrayLength = 0x40000000; if (maxArrayLength > MaximumArrayLength) { maxArrayLength = MaximumArrayLength; } else if (maxArrayLength < MinimumArrayLength) { maxArrayLength = MinimumArrayLength; } // Create the buckets. int poolId = Id; int maxBuckets = Utilities.SelectBucketIndex(maxArrayLength); var buckets = new Bucket[maxBuckets + 1]; for (int i = 0; i < buckets.Length; i++) { buckets[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, poolId); } _buckets = buckets; } /// Gets an ID for the pool to use with events. private int Id => GetHashCode(); public override T[] Rent(int minimumLength) { // Arrays can't be smaller than zero. We allow requesting zero-length arrays (even though // pooling such an array isn't valuable) as it's a valid length array, and we want the pool // to be usable in general instead of using `new`, even for computed lengths. if (minimumLength < 0) { throw new ArgumentOutOfRangeException(nameof(minimumLength)); } else if (minimumLength == 0) { // No need for events with the empty array. Our pool is effectively infinite // and we'll never allocate for rents and never store for returns. return Array.Empty(); } var log = ArrayPoolEventSource.Log; T[] buffer = null; int index = Utilities.SelectBucketIndex(minimumLength); if (index < _buckets.Length) { // Search for an array starting at the 'index' bucket. If the bucket is empty, bump up to the // next higher bucket and try that one, but only try at most a few buckets. const int MaxBucketsToTry = 2; int i = index; do { // Attempt to rent from the bucket. If we get a buffer from it, return it. buffer = _buckets[i].Rent(); if (buffer != null) { if (log.IsEnabled()) { log.BufferRented(buffer.GetHashCode(), buffer.Length, Id, _buckets[i].Id); } return buffer; } } while (++i < _buckets.Length && i != index + MaxBucketsToTry); // The pool was exhausted for this buffer size. Allocate a new buffer with a size corresponding // to the appropriate bucket. buffer = new T[_buckets[index]._bufferLength]; } else { // The request was for a size too large for the pool. Allocate an array of exactly the requested length. // When it's returned to the pool, we'll simply throw it away. buffer = new T[minimumLength]; } if (log.IsEnabled()) { int bufferId = buffer.GetHashCode(), bucketId = -1; // no bucket for an on-demand allocated buffer log.BufferRented(bufferId, buffer.Length, Id, bucketId); log.BufferAllocated(bufferId, buffer.Length, Id, bucketId, index >= _buckets.Length ? ArrayPoolEventSource.BufferAllocatedReason.OverMaximumSize : ArrayPoolEventSource.BufferAllocatedReason.PoolExhausted); } return buffer; } public override void Return(T[] array, bool clearArray = false) { if (array == null) { throw new ArgumentNullException(nameof(array)); } else if (array.Length == 0) { // Ignore empty arrays. When a zero-length array is rented, we return a singleton // rather than actually taking a buffer out of the lowest bucket. return; } // Determine with what bucket this array length is associated int bucket = Utilities.SelectBucketIndex(array.Length); // If we can tell that the buffer was allocated, drop it. Otherwise, check if we have space in the pool if (bucket < _buckets.Length) { // Clear the array if the user requests if (clearArray) { Array.Clear(array, 0, array.Length); } // Return the buffer to its bucket. In the future, we might consider having Return return false // instead of dropping a bucket, in which case we could try to return to a lower-sized bucket, // just as how in Rent we allow renting from a higher-sized bucket. _buckets[bucket].Return(array); } // Log that the buffer was returned var log = ArrayPoolEventSource.Log; if (log.IsEnabled()) { log.BufferReturned(array.GetHashCode(), array.Length, Id); } } /// Provides a thread-safe bucket containing buffers that can be Rent'd and Return'd. private sealed class Bucket { internal readonly int _bufferLength; private readonly T[][] _buffers; private readonly int _poolId; private SpinLock _lock; // do not make this readonly; it's a mutable struct private int _index; /// /// Creates the pool with numberOfBuffers arrays where each buffer is of bufferLength length. /// internal Bucket(int bufferLength, int numberOfBuffers, int poolId) { _lock = new SpinLock(Debugger.IsAttached); // only enable thread tracking if debugger is attached; it adds non-trivial overheads to Enter/Exit _buffers = new T[numberOfBuffers][]; _bufferLength = bufferLength; _poolId = poolId; } /// Gets an ID for the bucket to use with events. internal int Id => GetHashCode(); /// Takes an array from the bucket. If the bucket is empty, returns null. internal T[] Rent() { T[][] buffers = _buffers; T[] buffer = null; // While holding the lock, grab whatever is at the next available index and // update the index. We do as little work as possible while holding the spin // lock to minimize contention with other threads. The try/finally is // necessary to properly handle thread aborts on platforms which have them. bool lockTaken = false, allocateBuffer = false; try { _lock.Enter(ref lockTaken); if (_index < buffers.Length) { buffer = buffers[_index]; buffers[_index++] = null; allocateBuffer = buffer == null; } } finally { if (lockTaken) _lock.Exit(false); } // While we were holding the lock, we grabbed whatever was at the next available index, if // there was one. If we tried and if we got back null, that means we hadn't yet allocated // for that slot, in which case we should do so now. if (allocateBuffer) { buffer = new T[_bufferLength]; var log = ArrayPoolEventSource.Log; if (log.IsEnabled()) { log.BufferAllocated(buffer.GetHashCode(), _bufferLength, _poolId, Id, ArrayPoolEventSource.BufferAllocatedReason.Pooled); } } return buffer; } /// /// Attempts to return the buffer to the bucket. If successful, the buffer will be stored /// in the bucket and true will be returned; otherwise, the buffer won't be stored, and false /// will be returned. /// internal void Return(T[] array) { // Check to see if the buffer is the correct size for this bucket if (array.Length != _bufferLength) { throw new ArgumentException(SR.ArgumentException_BufferNotFromPool, nameof(array)); } // While holding the spin lock, if there's room available in the bucket, // put the buffer into the next available slot. Otherwise, we just drop it. // The try/finally is necessary to properly handle thread aborts on platforms // which have them. bool lockTaken = false; try { _lock.Enter(ref lockTaken); if (_index != 0) { _buffers[--_index] = array; } } finally { if (lockTaken) _lock.Exit(false); } } } } }