Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 91eb031

Browse files
jamesqojkotas
authored andcommitted
Minimize buffer allocations in Stream.CopyTo for seekable streams (#4540)
The current implementation of Stream.CopyTo allocates a giant, 81920-byte buffer if no bufferSize parameter is passed. This is incredibly wasteful if the stream we're copying from can seek, because then we can use the Length and Position properties to determine how many bytes are left and allocate a buffer of that size.
1 parent bdfce9e commit 91eb031

File tree

1 file changed

+58
-2
lines changed

1 file changed

+58
-2
lines changed

src/mscorlib/src/System/IO/Stream.cs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,40 @@ public virtual int WriteTimeout {
116116
[ComVisible(false)]
117117
public Task CopyToAsync(Stream destination)
118118
{
119-
return CopyToAsync(destination, _DefaultCopyBufferSize);
119+
int bufferSize = _DefaultCopyBufferSize;
120+
121+
#if FEATURE_CORECLR
122+
if (CanSeek)
123+
{
124+
long length = Length;
125+
long position = Position;
126+
if (length <= position) // Handles negative overflows
127+
{
128+
// If we go down this branch, it means there are
129+
// no bytes left in this stream.
130+
131+
// Ideally we would just return Task.CompletedTask here,
132+
// but CopyToAsync(Stream, int, CancellationToken) was already
133+
// virtual at the time this optimization was introduced. So
134+
// if it does things like argument validation (checking if destination
135+
// is null and throwing an exception), then await fooStream.CopyToAsync(null)
136+
// would no longer throw if there were no bytes left. On the other hand,
137+
// we also can't roll our own argument validation and return Task.CompletedTask,
138+
// because it would be a breaking change if the stream's override didn't throw before,
139+
// or in a different order. So for simplicity, we just set the bufferSize to 1
140+
// (not 0 since the default implementation throws for 0) and forward to the virtual method.
141+
bufferSize = 1;
142+
}
143+
else
144+
{
145+
long remaining = length - position;
146+
if (remaining > 0) // In the case of a positive overflow, stick to the default size
147+
bufferSize = (int)Math.Min(bufferSize, remaining);
148+
}
149+
}
150+
#endif // FEATURE_CORECLR
151+
152+
return CopyToAsync(destination, bufferSize);
120153
}
121154

122155
[HostProtection(ExternalThreading = true)]
@@ -155,7 +188,30 @@ private async Task CopyToAsyncInternal(Stream destination, Int32 bufferSize, Can
155188
// the current position.
156189
public void CopyTo(Stream destination)
157190
{
158-
CopyTo(destination, _DefaultCopyBufferSize);
191+
int bufferSize = _DefaultCopyBufferSize;
192+
193+
#if FEATURE_CORECLR
194+
if (CanSeek)
195+
{
196+
long length = Length;
197+
long position = Position;
198+
if (length <= position) // Handles negative overflows
199+
{
200+
// No bytes left in stream
201+
// Call the other overload with a bufferSize of 1,
202+
// in case it's made virtual in the future
203+
bufferSize = 1;
204+
}
205+
else
206+
{
207+
long remaining = length - position;
208+
if (remaining > 0) // In the case of a positive overflow, stick to the default size
209+
bufferSize = (int)Math.Min(bufferSize, remaining);
210+
}
211+
}
212+
#endif // FEATURE_CORECLR
213+
214+
CopyTo(destination, bufferSize);
159215
}
160216

161217
public void CopyTo(Stream destination, int bufferSize)

0 commit comments

Comments
 (0)