Skip to content

Commit fa779e8

Browse files
authored
Raise the max size poolable by ArrayPool (#55621)
We previously set an arbitrary cut-off of 2MB max array size. That was before we would trim arrays in response to memory pressure. This now raises the limit as high as we can while maintaining the current power-of-two-based scheme.
1 parent 654dbc4 commit fa779e8

File tree

5 files changed

+45
-20
lines changed

5 files changed

+45
-20
lines changed

src/libraries/System.Buffers/tests/ArrayPool/UnitTests.cs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Numerics;
99
using System.Threading.Tasks;
1010
using Microsoft.DotNet.RemoteExecutor;
11+
using Microsoft.DotNet.XUnitExtensions;
1112
using Xunit;
1213

1314
namespace System.Buffers.ArrayPool.Tests
@@ -240,15 +241,11 @@ public static void NewDefaultArrayPoolWithSmallBufferSizeRoundsToOurSmallestSupp
240241
}
241242

242243
[Fact]
243-
public static void ReturningABufferGreaterThanMaxSizeDoesNotThrow()
244+
public static void ReturningToCreatePoolABufferGreaterThanMaxSizeDoesNotThrow()
244245
{
245246
ArrayPool<byte> pool = ArrayPool<byte>.Create(maxArrayLength: 16, maxArraysPerBucket: 1);
246247
byte[] rented = pool.Rent(32);
247248
pool.Return(rented);
248-
249-
ArrayPool<byte>.Shared.Return(new byte[3 * 1024 * 1024]);
250-
ArrayPool<char>.Shared.Return(new char[3 * 1024 * 1024]);
251-
ArrayPool<string>.Shared.Return(new string[3 * 1024 * 1024]);
252249
}
253250

254251
[Fact]
@@ -292,11 +289,11 @@ public static void CanRentManySizedBuffers(ArrayPool<byte> pool)
292289
[InlineData(1024, 1024)]
293290
[InlineData(4096, 4096)]
294291
[InlineData(1024 * 1024, 1024 * 1024)]
295-
[InlineData(1024 * 1024 + 1, 1024 * 1024 + 1)]
292+
[InlineData(1024 * 1024 + 1, 1024 * 1024 * 2)]
296293
[InlineData(1024 * 1024 * 2, 1024 * 1024 * 2)]
297294
public static void RentingSpecificLengthsYieldsExpectedLengths(int requestedMinimum, int expectedLength)
298295
{
299-
foreach (ArrayPool<byte> pool in new[] { ArrayPool<byte>.Create(), ArrayPool<byte>.Shared })
296+
foreach (ArrayPool<byte> pool in new[] { ArrayPool<byte>.Create((int)BitOperations.RoundUpToPowerOf2((uint)requestedMinimum), 1), ArrayPool<byte>.Shared })
300297
{
301298
byte[] buffer1 = pool.Rent(requestedMinimum);
302299
byte[] buffer2 = pool.Rent(requestedMinimum);
@@ -313,7 +310,7 @@ public static void RentingSpecificLengthsYieldsExpectedLengths(int requestedMini
313310
pool.Return(buffer1);
314311
}
315312

316-
foreach (ArrayPool<char> pool in new[] { ArrayPool<char>.Create(), ArrayPool<char>.Shared })
313+
foreach (ArrayPool<char> pool in new[] { ArrayPool<char>.Create((int)BitOperations.RoundUpToPowerOf2((uint)requestedMinimum), 1), ArrayPool<char>.Shared })
317314
{
318315
char[] buffer1 = pool.Rent(requestedMinimum);
319316
char[] buffer2 = pool.Rent(requestedMinimum);
@@ -330,7 +327,7 @@ public static void RentingSpecificLengthsYieldsExpectedLengths(int requestedMini
330327
pool.Return(buffer1);
331328
}
332329

333-
foreach (ArrayPool<string> pool in new[] { ArrayPool<string>.Create(), ArrayPool<string>.Shared })
330+
foreach (ArrayPool<string> pool in new[] { ArrayPool<string>.Create((int)BitOperations.RoundUpToPowerOf2((uint)requestedMinimum), 1), ArrayPool<string>.Shared })
334331
{
335332
string[] buffer1 = pool.Rent(requestedMinimum);
336333
string[] buffer2 = pool.Rent(requestedMinimum);
@@ -348,6 +345,38 @@ public static void RentingSpecificLengthsYieldsExpectedLengths(int requestedMini
348345
}
349346
}
350347

348+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess))]
349+
[InlineData(1024 * 1024 * 1024 - 1, true)]
350+
[InlineData(1024 * 1024 * 1024, true)]
351+
[InlineData(1024 * 1024 * 1024 + 1, false)]
352+
[InlineData(0X7FFFFFC7 /* Array.MaxLength */, false)]
353+
[OuterLoop]
354+
public static void RentingGiganticArraySucceeds(int length, bool expectPooled)
355+
{
356+
var options = new RemoteInvokeOptions();
357+
options.StartInfo.UseShellExecute = false;
358+
options.StartInfo.EnvironmentVariables.Add(TrimSwitchName, "false");
359+
360+
RemoteExecutor.Invoke((lengthStr, expectPooledStr) =>
361+
{
362+
int length = int.Parse(lengthStr);
363+
byte[] array;
364+
try
365+
{
366+
array = ArrayPool<byte>.Shared.Rent(length);
367+
}
368+
catch (OutOfMemoryException)
369+
{
370+
return;
371+
}
372+
373+
Assert.InRange(array.Length, length, int.MaxValue);
374+
ArrayPool<byte>.Shared.Return(array);
375+
376+
Assert.Equal(bool.Parse(expectPooledStr), ReferenceEquals(array, ArrayPool<byte>.Shared.Rent(length)));
377+
}, length.ToString(), expectPooled.ToString(), options).Dispose();
378+
}
379+
351380
[Fact]
352381
public static void RentingAfterPoolExhaustionReturnsSizeForCorrespondingBucket_SmallerThanLimit()
353382
{

src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/Compression/WebSocketDeflater.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,11 @@ public ReadOnlySpan<byte> Deflate(ReadOnlySpan<byte> payload, bool endOfMessage)
4646
{
4747
Debug.Assert(_buffer is null, "Invalid state, ReleaseBuffer not called.");
4848

49-
// Do not try to rent more than 1MB initially, because it will actually allocate
50-
// instead of renting. Be optimistic that what we're sending is actually going to fit.
51-
const int MaxInitialBufferLength = 1024 * 1024;
52-
5349
// For small payloads there might actually be overhead in the compression and the resulting
5450
// output might be larger than the payload. This is why we rent at least 4KB initially.
5551
const int MinInitialBufferLength = 4 * 1024;
5652

57-
_buffer = ArrayPool<byte>.Shared.Rent(Math.Clamp(payload.Length, MinInitialBufferLength, MaxInitialBufferLength));
53+
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(payload.Length, MinInitialBufferLength));
5854
int position = 0;
5955

6056
while (true)

src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/Compression/WebSocketInflater.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,9 @@ public void Prepare(long payloadLength, int userBufferLength)
8282
}
8383
else
8484
{
85-
// Rent a buffer as close to the size of the user buffer as possible,
86-
// but not try to rent anything above 1MB because the array pool will allocate.
85+
// Rent a buffer as close to the size of the user buffer as possible.
8786
// If the payload is smaller than the user buffer, rent only as much as we need.
88-
_buffer = ArrayPool<byte>.Shared.Rent(Math.Min(userBufferLength, (int)Math.Min(payloadLength, 1024 * 1024)));
87+
_buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(userBufferLength, payloadLength));
8988
}
9089
}
9190

src/libraries/System.Private.CoreLib/src/System/Buffers/TlsOverPerCoreLockedStacksArrayPool.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@ internal sealed partial class TlsOverPerCoreLockedStacksArrayPool<T> : ArrayPool
2424
// TODO https://github.com/dotnet/coreclr/pull/7747: "Investigate optimizing ArrayPool heuristics"
2525
// - Explore caching in TLS more than one array per size per thread, and moving stale buffers to the global queue.
2626
// - Explore changing the size of each per-core bucket, potentially dynamically or based on other factors like array size.
27-
// - Explore changing number of buckets and what sizes of arrays are cached.
2827
// - Investigate whether false sharing is causing any issues, in particular on LockedStack's count and the contents of its array.
2928
// ...
3029

3130
/// <summary>The number of buckets (array sizes) in the pool, one for each array length, starting from length 16.</summary>
32-
private const int NumBuckets = 17; // Utilities.SelectBucketIndex(2*1024*1024)
31+
private const int NumBuckets = 27; // Utilities.SelectBucketIndex(1024 * 1024 * 1024 + 1)
3332
/// <summary>Maximum number of per-core stacks to use per array size.</summary>
3433
private const int MaxPerCorePerArraySizeStacks = 64; // selected to avoid needing to worry about processor groups
3534
/// <summary>The maximum number of buffers to store in a bucket's global queue.</summary>

src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,9 @@ private unsafe int IcuGetHashCodeOfString(ReadOnlySpan<char> source, CompareOpti
722722

723723
// according to ICU User Guide the performance of ucol_getSortKey is worse when it is called with null output buffer
724724
// the solution is to try to fill the sort key in a temporary buffer of size equal 4 x string length
725-
// 1MB is the biggest array that can be rented from ArrayPool.Shared without memory allocation
725+
// (The ArrayPool used to have a limit on the length of buffers it would cache; this code was avoiding
726+
// exceeding that limit to avoid a per-operation allocation, and the performance implications here
727+
// were not re-evaluated when the limit was lifted.)
726728
int sortKeyLength = (source.Length > 1024 * 1024 / 4) ? 0 : 4 * source.Length;
727729

728730
byte[]? borrowedArray = null;

0 commit comments

Comments
 (0)