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