1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Diagnostics; 6 using System.Threading; 7 8 namespace System.Buffers 9 { 10 internal sealed partial class ConfigurableArrayPool<T> : ArrayPool<T> 11 { 12 /// <summary>The default maximum length of each array in the pool (2^20).</summary> 13 private const int DefaultMaxArrayLength = 1024 * 1024; 14 /// <summary>The default maximum number of arrays per bucket that are available for rent.</summary> 15 private const int DefaultMaxNumberOfArraysPerBucket = 50; 16 17 private readonly Bucket[] _buckets; 18 ConfigurableArrayPool()19 internal ConfigurableArrayPool() : this(DefaultMaxArrayLength, DefaultMaxNumberOfArraysPerBucket) 20 { 21 } 22 ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)23 internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket) 24 { 25 if (maxArrayLength <= 0) 26 { 27 throw new ArgumentOutOfRangeException(nameof(maxArrayLength)); 28 } 29 if (maxArraysPerBucket <= 0) 30 { 31 throw new ArgumentOutOfRangeException(nameof(maxArraysPerBucket)); 32 } 33 34 // Our bucketing algorithm has a min length of 2^4 and a max length of 2^30. 35 // Constrain the actual max used to those values. 36 const int MinimumArrayLength = 0x10, MaximumArrayLength = 0x40000000; 37 if (maxArrayLength > MaximumArrayLength) 38 { 39 maxArrayLength = MaximumArrayLength; 40 } 41 else if (maxArrayLength < MinimumArrayLength) 42 { 43 maxArrayLength = MinimumArrayLength; 44 } 45 46 // Create the buckets. 47 int poolId = Id; 48 int maxBuckets = Utilities.SelectBucketIndex(maxArrayLength); 49 var buckets = new Bucket[maxBuckets + 1]; 50 for (int i = 0; i < buckets.Length; i++) 51 { 52 buckets[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, poolId); 53 } 54 _buckets = buckets; 55 } 56 57 /// <summary>Gets an ID for the pool to use with events.</summary> 58 private int Id => GetHashCode(); 59 Rent(int minimumLength)60 public override T[] Rent(int minimumLength) 61 { 62 // Arrays can't be smaller than zero. We allow requesting zero-length arrays (even though 63 // pooling such an array isn't valuable) as it's a valid length array, and we want the pool 64 // to be usable in general instead of using `new`, even for computed lengths. 65 if (minimumLength < 0) 66 { 67 throw new ArgumentOutOfRangeException(nameof(minimumLength)); 68 } 69 else if (minimumLength == 0) 70 { 71 // No need for events with the empty array. Our pool is effectively infinite 72 // and we'll never allocate for rents and never store for returns. 73 return Array.Empty<T>(); 74 } 75 76 var log = ArrayPoolEventSource.Log; 77 T[] buffer = null; 78 79 int index = Utilities.SelectBucketIndex(minimumLength); 80 if (index < _buckets.Length) 81 { 82 // Search for an array starting at the 'index' bucket. If the bucket is empty, bump up to the 83 // next higher bucket and try that one, but only try at most a few buckets. 84 const int MaxBucketsToTry = 2; 85 int i = index; 86 do 87 { 88 // Attempt to rent from the bucket. If we get a buffer from it, return it. 89 buffer = _buckets[i].Rent(); 90 if (buffer != null) 91 { 92 if (log.IsEnabled()) 93 { 94 log.BufferRented(buffer.GetHashCode(), buffer.Length, Id, _buckets[i].Id); 95 } 96 return buffer; 97 } 98 } 99 while (++i < _buckets.Length && i != index + MaxBucketsToTry); 100 101 // The pool was exhausted for this buffer size. Allocate a new buffer with a size corresponding 102 // to the appropriate bucket. 103 buffer = new T[_buckets[index]._bufferLength]; 104 } 105 else 106 { 107 // The request was for a size too large for the pool. Allocate an array of exactly the requested length. 108 // When it's returned to the pool, we'll simply throw it away. 109 buffer = new T[minimumLength]; 110 } 111 112 if (log.IsEnabled()) 113 { 114 int bufferId = buffer.GetHashCode(), bucketId = -1; // no bucket for an on-demand allocated buffer 115 log.BufferRented(bufferId, buffer.Length, Id, bucketId); 116 log.BufferAllocated(bufferId, buffer.Length, Id, bucketId, index >= _buckets.Length ? 117 ArrayPoolEventSource.BufferAllocatedReason.OverMaximumSize : ArrayPoolEventSource.BufferAllocatedReason.PoolExhausted); 118 } 119 120 return buffer; 121 } 122 Return(T[] array, bool clearArray = false)123 public override void Return(T[] array, bool clearArray = false) 124 { 125 if (array == null) 126 { 127 throw new ArgumentNullException(nameof(array)); 128 } 129 else if (array.Length == 0) 130 { 131 // Ignore empty arrays. When a zero-length array is rented, we return a singleton 132 // rather than actually taking a buffer out of the lowest bucket. 133 return; 134 } 135 136 // Determine with what bucket this array length is associated 137 int bucket = Utilities.SelectBucketIndex(array.Length); 138 139 // If we can tell that the buffer was allocated, drop it. Otherwise, check if we have space in the pool 140 if (bucket < _buckets.Length) 141 { 142 // Clear the array if the user requests 143 if (clearArray) 144 { 145 Array.Clear(array, 0, array.Length); 146 } 147 148 // Return the buffer to its bucket. In the future, we might consider having Return return false 149 // instead of dropping a bucket, in which case we could try to return to a lower-sized bucket, 150 // just as how in Rent we allow renting from a higher-sized bucket. 151 _buckets[bucket].Return(array); 152 } 153 154 // Log that the buffer was returned 155 var log = ArrayPoolEventSource.Log; 156 if (log.IsEnabled()) 157 { 158 log.BufferReturned(array.GetHashCode(), array.Length, Id); 159 } 160 } 161 162 /// <summary>Provides a thread-safe bucket containing buffers that can be Rent'd and Return'd.</summary> 163 private sealed class Bucket 164 { 165 internal readonly int _bufferLength; 166 private readonly T[][] _buffers; 167 private readonly int _poolId; 168 169 private SpinLock _lock; // do not make this readonly; it's a mutable struct 170 private int _index; 171 172 /// <summary> 173 /// Creates the pool with numberOfBuffers arrays where each buffer is of bufferLength length. 174 /// </summary> Bucket(int bufferLength, int numberOfBuffers, int poolId)175 internal Bucket(int bufferLength, int numberOfBuffers, int poolId) 176 { 177 _lock = new SpinLock(Debugger.IsAttached); // only enable thread tracking if debugger is attached; it adds non-trivial overheads to Enter/Exit 178 _buffers = new T[numberOfBuffers][]; 179 _bufferLength = bufferLength; 180 _poolId = poolId; 181 } 182 183 /// <summary>Gets an ID for the bucket to use with events.</summary> 184 internal int Id => GetHashCode(); 185 186 /// <summary>Takes an array from the bucket. If the bucket is empty, returns null.</summary> Rent()187 internal T[] Rent() 188 { 189 T[][] buffers = _buffers; 190 T[] buffer = null; 191 192 // While holding the lock, grab whatever is at the next available index and 193 // update the index. We do as little work as possible while holding the spin 194 // lock to minimize contention with other threads. The try/finally is 195 // necessary to properly handle thread aborts on platforms which have them. 196 bool lockTaken = false, allocateBuffer = false; 197 try 198 { 199 _lock.Enter(ref lockTaken); 200 201 if (_index < buffers.Length) 202 { 203 buffer = buffers[_index]; 204 buffers[_index++] = null; 205 allocateBuffer = buffer == null; 206 } 207 } 208 finally 209 { 210 if (lockTaken) _lock.Exit(false); 211 } 212 213 // While we were holding the lock, we grabbed whatever was at the next available index, if 214 // there was one. If we tried and if we got back null, that means we hadn't yet allocated 215 // for that slot, in which case we should do so now. 216 if (allocateBuffer) 217 { 218 buffer = new T[_bufferLength]; 219 220 var log = ArrayPoolEventSource.Log; 221 if (log.IsEnabled()) 222 { 223 log.BufferAllocated(buffer.GetHashCode(), _bufferLength, _poolId, Id, 224 ArrayPoolEventSource.BufferAllocatedReason.Pooled); 225 } 226 } 227 228 return buffer; 229 } 230 231 /// <summary> 232 /// Attempts to return the buffer to the bucket. If successful, the buffer will be stored 233 /// in the bucket and true will be returned; otherwise, the buffer won't be stored, and false 234 /// will be returned. 235 /// </summary> Return(T[] array)236 internal void Return(T[] array) 237 { 238 // Check to see if the buffer is the correct size for this bucket 239 if (array.Length != _bufferLength) 240 { 241 throw new ArgumentException(SR.ArgumentException_BufferNotFromPool, nameof(array)); 242 } 243 244 // While holding the spin lock, if there's room available in the bucket, 245 // put the buffer into the next available slot. Otherwise, we just drop it. 246 // The try/finally is necessary to properly handle thread aborts on platforms 247 // which have them. 248 bool lockTaken = false; 249 try 250 { 251 _lock.Enter(ref lockTaken); 252 253 if (_index != 0) 254 { 255 _buffers[--_index] = array; 256 } 257 } 258 finally 259 { 260 if (lockTaken) _lock.Exit(false); 261 } 262 } 263 } 264 } 265 } 266