ConfigurableArrayPool.cs 11 KB

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