Skip to content

Commit

Permalink
Raise the max size poolable by ArrayPool (#55621)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stephentoub authored Jul 14, 2021
1 parent 654dbc4 commit fa779e8
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 20 deletions.
47 changes: 38 additions & 9 deletions src/libraries/System.Buffers/tests/ArrayPool/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Numerics;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.DotNet.XUnitExtensions;
using Xunit;

namespace System.Buffers.ArrayPool.Tests
Expand Down Expand Up @@ -240,15 +241,11 @@ public static void NewDefaultArrayPoolWithSmallBufferSizeRoundsToOurSmallestSupp
}

[Fact]
public static void ReturningABufferGreaterThanMaxSizeDoesNotThrow()
public static void ReturningToCreatePoolABufferGreaterThanMaxSizeDoesNotThrow()
{
ArrayPool<byte> pool = ArrayPool<byte>.Create(maxArrayLength: 16, maxArraysPerBucket: 1);
byte[] rented = pool.Rent(32);
pool.Return(rented);

ArrayPool<byte>.Shared.Return(new byte[3 * 1024 * 1024]);
ArrayPool<char>.Shared.Return(new char[3 * 1024 * 1024]);
ArrayPool<string>.Shared.Return(new string[3 * 1024 * 1024]);
}

[Fact]
Expand Down Expand Up @@ -292,11 +289,11 @@ public static void CanRentManySizedBuffers(ArrayPool<byte> pool)
[InlineData(1024, 1024)]
[InlineData(4096, 4096)]
[InlineData(1024 * 1024, 1024 * 1024)]
[InlineData(1024 * 1024 + 1, 1024 * 1024 + 1)]
[InlineData(1024 * 1024 + 1, 1024 * 1024 * 2)]
[InlineData(1024 * 1024 * 2, 1024 * 1024 * 2)]
public static void RentingSpecificLengthsYieldsExpectedLengths(int requestedMinimum, int expectedLength)
{
foreach (ArrayPool<byte> pool in new[] { ArrayPool<byte>.Create(), ArrayPool<byte>.Shared })
foreach (ArrayPool<byte> pool in new[] { ArrayPool<byte>.Create((int)BitOperations.RoundUpToPowerOf2((uint)requestedMinimum), 1), ArrayPool<byte>.Shared })
{
byte[] buffer1 = pool.Rent(requestedMinimum);
byte[] buffer2 = pool.Rent(requestedMinimum);
Expand All @@ -313,7 +310,7 @@ public static void RentingSpecificLengthsYieldsExpectedLengths(int requestedMini
pool.Return(buffer1);
}

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

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

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.Is64BitProcess))]
[InlineData(1024 * 1024 * 1024 - 1, true)]
[InlineData(1024 * 1024 * 1024, true)]
[InlineData(1024 * 1024 * 1024 + 1, false)]
[InlineData(0X7FFFFFC7 /* Array.MaxLength */, false)]
[OuterLoop]
public static void RentingGiganticArraySucceeds(int length, bool expectPooled)
{
var options = new RemoteInvokeOptions();
options.StartInfo.UseShellExecute = false;
options.StartInfo.EnvironmentVariables.Add(TrimSwitchName, "false");

RemoteExecutor.Invoke((lengthStr, expectPooledStr) =>
{
int length = int.Parse(lengthStr);
byte[] array;
try
{
array = ArrayPool<byte>.Shared.Rent(length);
}
catch (OutOfMemoryException)
{
return;
}

Assert.InRange(array.Length, length, int.MaxValue);
ArrayPool<byte>.Shared.Return(array);

Assert.Equal(bool.Parse(expectPooledStr), ReferenceEquals(array, ArrayPool<byte>.Shared.Rent(length)));
}, length.ToString(), expectPooled.ToString(), options).Dispose();
}

[Fact]
public static void RentingAfterPoolExhaustionReturnsSizeForCorrespondingBucket_SmallerThanLimit()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,11 @@ public ReadOnlySpan<byte> Deflate(ReadOnlySpan<byte> payload, bool endOfMessage)
{
Debug.Assert(_buffer is null, "Invalid state, ReleaseBuffer not called.");

// Do not try to rent more than 1MB initially, because it will actually allocate
// instead of renting. Be optimistic that what we're sending is actually going to fit.
const int MaxInitialBufferLength = 1024 * 1024;

// For small payloads there might actually be overhead in the compression and the resulting
// output might be larger than the payload. This is why we rent at least 4KB initially.
const int MinInitialBufferLength = 4 * 1024;

_buffer = ArrayPool<byte>.Shared.Rent(Math.Clamp(payload.Length, MinInitialBufferLength, MaxInitialBufferLength));
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(payload.Length, MinInitialBufferLength));
int position = 0;

while (true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ public void Prepare(long payloadLength, int userBufferLength)
}
else
{
// Rent a buffer as close to the size of the user buffer as possible,
// but not try to rent anything above 1MB because the array pool will allocate.
// Rent a buffer as close to the size of the user buffer as possible.
// If the payload is smaller than the user buffer, rent only as much as we need.
_buffer = ArrayPool<byte>.Shared.Rent(Math.Min(userBufferLength, (int)Math.Min(payloadLength, 1024 * 1024)));
_buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(userBufferLength, payloadLength));
}
}

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

/// <summary>The number of buckets (array sizes) in the pool, one for each array length, starting from length 16.</summary>
private const int NumBuckets = 17; // Utilities.SelectBucketIndex(2*1024*1024)
private const int NumBuckets = 27; // Utilities.SelectBucketIndex(1024 * 1024 * 1024 + 1)
/// <summary>Maximum number of per-core stacks to use per array size.</summary>
private const int MaxPerCorePerArraySizeStacks = 64; // selected to avoid needing to worry about processor groups
/// <summary>The maximum number of buffers to store in a bucket's global queue.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,9 @@ private unsafe int IcuGetHashCodeOfString(ReadOnlySpan<char> source, CompareOpti

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

byte[]? borrowedArray = null;
Expand Down

0 comments on commit fa779e8

Please sign in to comment.