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