Skip to content

Commit 035b729

Browse files
authored
Improve ArrayBufferWriter re-alloc perf when size is > int.MaxSize / 2 (#42950)
1 parent b04361a commit 035b729

File tree

6 files changed

+57
-14
lines changed

6 files changed

+57
-14
lines changed

src/libraries/Common/src/System/Buffers/ArrayBufferWriter.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ namespace System.Buffers
1515
#endif
1616
sealed class ArrayBufferWriter<T> : IBufferWriter<T>
1717
{
18+
// Copy of Array.MaxArrayLength. For byte arrays the limit is slightly larger
19+
private const int MaxArrayLength = 0X7FEFFFFF;
20+
21+
private const int DefaultInitialBufferSize = 256;
22+
1823
private T[] _buffer;
1924
private int _index;
2025

21-
private const int DefaultInitialBufferSize = 256;
2226

2327
/// <summary>
2428
/// Creates an instance of an <see cref="ArrayBufferWriter{T}"/>, in which data can be written to,
@@ -167,6 +171,8 @@ private void CheckAndResizeBuffer(int sizeHint)
167171
if (sizeHint > FreeCapacity)
168172
{
169173
int currentLength = _buffer.Length;
174+
175+
// Attempt to grow by the larger of the sizeHint and double the current size.
170176
int growBy = Math.Max(sizeHint, currentLength);
171177

172178
if (currentLength == 0)
@@ -178,11 +184,16 @@ private void CheckAndResizeBuffer(int sizeHint)
178184

179185
if ((uint)newSize > int.MaxValue)
180186
{
181-
newSize = currentLength + sizeHint;
182-
if ((uint)newSize > int.MaxValue)
187+
// Attempt to grow to MaxArrayLength.
188+
uint needed = (uint)(currentLength - FreeCapacity + sizeHint);
189+
Debug.Assert(needed > currentLength);
190+
191+
if (needed > MaxArrayLength)
183192
{
184-
ThrowOutOfMemoryException((uint)newSize);
193+
ThrowOutOfMemoryException(needed);
185194
}
195+
196+
newSize = MaxArrayLength;
186197
}
187198

188199
Array.Resize(ref _buffer, newSize);

src/libraries/System.Memory/tests/ArrayBufferWriter/ArrayBufferWriterTests.Byte.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,20 @@ public async Task WriteAndCopyToStreamAsync()
9898
[OuterLoop]
9999
public void GetMemory_ExceedMaximumBufferSize()
100100
{
101-
var output = new ArrayBufferWriter<byte>(int.MaxValue / 2 + 1);
102-
output.Advance(int.MaxValue / 2 + 1);
103-
Memory<byte> memory = output.GetMemory(1); // Validate we can't double the buffer size, but can grow by sizeHint
104-
Assert.Equal(1, memory.Length);
101+
const int MaxArrayLength = 0X7FEFFFFF;
102+
103+
int initialCapacity = int.MaxValue / 2 + 1;
104+
105+
var output = new ArrayBufferWriter<byte>(initialCapacity);
106+
output.Advance(initialCapacity);
107+
108+
// Validate we can't double the buffer size, but can grow
109+
Memory<byte> memory = output.GetMemory(1);
110+
111+
// The buffer should grow more than the 1 byte requested otherwise performance will not be usable
112+
// between 1GB and 2GB. The current implementation maxes out the buffer size to MaxArrayLength.
113+
Assert.Equal(MaxArrayLength - initialCapacity, memory.Length);
114+
105115
Assert.Throws<OutOfMemoryException>(() => output.GetMemory(int.MaxValue));
106116
}
107117
}

src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ namespace System.Text.Json
1111
{
1212
internal static partial class JsonHelpers
1313
{
14+
// Copy of Array.MaxArrayLength. For byte arrays the limit is slightly larger
15+
private const int MaxArrayLength = 0X7FEFFFFF;
16+
1417
/// <summary>
1518
/// Returns the span for the given reader.
1619
/// </summary>
@@ -143,5 +146,14 @@ public static bool IsValidNumberHandlingValue(JsonNumberHandling handling) =>
143146
JsonNumberHandling.AllowReadingFromString |
144147
JsonNumberHandling.WriteAsString |
145148
JsonNumberHandling.AllowNamedFloatingPointLiterals));
149+
150+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
151+
public static void ValidateInt32MaxArrayLength(uint length)
152+
{
153+
if (length > MaxArrayLength)
154+
{
155+
ThrowHelper.ThrowOutOfMemoryException(length);
156+
}
157+
}
146158
}
147159
}

src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,12 @@ public static InvalidOperationException GetInvalidOperationException(ExceptionRe
506506
return ex;
507507
}
508508

509+
[DoesNotReturn]
510+
public static void ThrowOutOfMemoryException(uint capacity)
511+
{
512+
throw new OutOfMemoryException(SR.Format(SR.BufferMaximumSizeExceeded, capacity));
513+
}
514+
509515
// This function will convert an ExceptionResource enum value to the resource string.
510516
[MethodImpl(MethodImplOptions.NoInlining)]
511517
private static string GetResourceString(ExceptionResource resource, int currentDepth, byte token, JsonTokenType tokenType)

src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,10 @@ private void Grow(int requiredSize)
10041004
{
10051005
Debug.Assert(_arrayBufferWriter != null);
10061006

1007-
_memory = _arrayBufferWriter.GetMemory(checked(BytesPending + sizeHint));
1007+
int needed = BytesPending + sizeHint;
1008+
JsonHelpers.ValidateInt32MaxArrayLength((uint)needed);
1009+
1010+
_memory = _arrayBufferWriter.GetMemory(needed);
10081011

10091012
Debug.Assert(_memory.Length >= sizeHint);
10101013
}

src/libraries/System.Text.Json/tests/Utf8JsonWriterTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -744,24 +744,25 @@ public void WriteLargeJsonToStreamWithoutFlushing()
744744
}
745745
Assert.Equal(150_097_503, writer.BytesPending);
746746

747-
for (int i = 0; i < 6; i++)
747+
for (int i = 0; i < 13; i++)
748748
{
749749
writer.WriteStringValue(text3);
750750
}
751-
Assert.Equal(1_050_097_521, writer.BytesPending);
751+
Assert.Equal(2_100_097_542, writer.BytesPending);
752+
753+
// Next write forces a grow beyond max array length
752754

753-
// Next write forces a grow beyond 2 GB
754755
Assert.Throws<OutOfMemoryException>(() => writer.WriteStringValue(text3));
755756

756-
Assert.Equal(1_050_097_521, writer.BytesPending);
757+
Assert.Equal(2_100_097_542, writer.BytesPending);
757758

758759
var text4 = JsonEncodedText.Encode(largeArray.AsSpan(0, 1));
759760
for (int i = 0; i < 10_000_000; i++)
760761
{
761762
writer.WriteStringValue(text4);
762763
}
763764

764-
Assert.Equal(1_050_097_521 + (4 * 10_000_000), writer.BytesPending);
765+
Assert.Equal(2_100_097_542 + (4 * 10_000_000), writer.BytesPending);
765766
}
766767
}
767768

0 commit comments

Comments
 (0)