-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Adding Buffer Pooling to System.IO #5742
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,9 @@ | |
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Text; | ||
using System.Buffers; | ||
using System.Diagnostics; | ||
using System.Text; | ||
|
||
namespace System.IO | ||
{ | ||
|
@@ -33,7 +34,7 @@ public class BinaryWriter : IDisposable | |
protected BinaryWriter() | ||
{ | ||
OutStream = Stream.Null; | ||
_buffer = new byte[16]; | ||
_buffer = ArrayPool<byte>.Shared.Rent(16); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it still make sense to rent in the ctor? Why isn't it lazy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There would be a lot of duplicate code since lots of different functions use _buffer, so I thought it made it simpler to initialize it in the ctor, especially since it is a trivially small array. |
||
_encoding = new UTF8Encoding(false, true); | ||
_encoder = _encoding.GetEncoder(); | ||
} | ||
|
@@ -62,7 +63,7 @@ public BinaryWriter(Stream output, Encoding encoding, bool leaveOpen) | |
} | ||
|
||
OutStream = output; | ||
_buffer = new byte[16]; | ||
_buffer = ArrayPool<byte>.Shared.Rent(16); | ||
_encoding = encoding; | ||
_encoder = _encoding.GetEncoder(); | ||
_leaveOpen = leaveOpen; | ||
|
@@ -81,6 +82,18 @@ protected virtual void Dispose(bool disposing) | |
OutStream.Dispose(); | ||
} | ||
} | ||
|
||
if (_buffer != null) | ||
{ | ||
ArrayPool<byte>.Shared.Return(_buffer); | ||
_buffer = null; | ||
} | ||
|
||
if (_largeByteBuffer != null) | ||
{ | ||
ArrayPool<byte>.Shared.Return(_largeByteBuffer); | ||
_largeByteBuffer = null; | ||
} | ||
} | ||
|
||
public void Dispose() | ||
|
@@ -371,7 +384,7 @@ public unsafe virtual void Write(string value) | |
|
||
if (_largeByteBuffer == null) | ||
{ | ||
_largeByteBuffer = new byte[LargeByteBufferSize]; | ||
_largeByteBuffer = ArrayPool<byte>.Shared.Rent(LargeByteBufferSize); | ||
_maxChars = LargeByteBufferSize / _encoding.GetMaxByteCount(1); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,12 @@ | |
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Runtime.InteropServices; | ||
using System.Buffers; | ||
using System.Diagnostics; | ||
using System.Diagnostics.Contracts; | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using System.Diagnostics.Contracts; | ||
|
||
namespace System.IO | ||
{ | ||
|
@@ -133,8 +134,9 @@ private void EnsureShadowBufferAllocated() | |
if (_buffer.Length != _bufferSize || _bufferSize >= MaxShadowBufferSize) | ||
return; | ||
|
||
byte[] shadowBuffer = new byte[Math.Min(_bufferSize + _bufferSize, MaxShadowBufferSize)]; | ||
byte[] shadowBuffer = ArrayPool<byte>.Shared.Rent(Math.Min(_bufferSize + _bufferSize, MaxShadowBufferSize)); | ||
Array.Copy(_buffer, 0, shadowBuffer, 0, _writePos); | ||
ArrayPool<byte>.Shared.Return(_buffer); | ||
_buffer = shadowBuffer; | ||
} | ||
|
||
|
@@ -144,7 +146,7 @@ private void EnsureBufferAllocated() | |
|
||
// BufferedStream is not intended for multi-threaded use, so no worries about the get/set race on _buffer. | ||
if (_buffer == null) | ||
_buffer = new byte[_bufferSize]; | ||
_buffer = ArrayPool<byte>.Shared.Rent(_bufferSize); | ||
} | ||
|
||
public override bool CanRead | ||
|
@@ -233,7 +235,11 @@ protected override void Dispose(bool disposing) | |
finally | ||
{ | ||
_stream = null; | ||
_buffer = null; | ||
if (_buffer != null) | ||
{ | ||
ArrayPool<byte>.Shared.Return(_buffer); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noted this offline, but I think that: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't users hold onto buffers in the pool anyways (after they've been "returned"), and read from them regardless? Or do we protect from that in the implementation somehow? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this isn't about complete security; after all, this all also in the same process, in the same app domain, code could easily read from whatever memory it wanted to, etc. This is more about minimizing unexpected consequences. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I understand what you're saying, but won't people need to expect to get already-used buffers when using the .Shared pool? It seems like it's part of the contract that rented buffers might not be cleared. What unexpected consequences are you referring to? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify, I think we should probably just clear the buffers here to take the conservative route, but I'm also just trying to figure out how we are thinking about this buffer pooling stuff going forward, and what our best practices are going to be, what we should be looking out for, etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
e.g. you load a .zip file using System.IO.Compression, and it contains some passwords. Those passwords are now in the buffers. Those buffers get returned to the pool, no one else uses the buffers from the pool, and they remain there for the remainder of the lifetime of the process. Previously they would have been much more likely to be collected and overwritten on the heap, but now they remain fresh. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll open an issue for this |
||
_buffer = null; | ||
} | ||
|
||
// Call base.Dispose(bool) to cleanup async IO resources | ||
base.Dispose(disposing); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Buffers; | ||
using System.Diagnostics; | ||
using System.Diagnostics.Contracts; | ||
using System.Threading; | ||
|
@@ -139,11 +140,18 @@ private async Task CopyToAsyncInternal(Stream destination, int bufferSize, Cance | |
Debug.Assert(CanRead); | ||
Debug.Assert(destination.CanWrite); | ||
|
||
byte[] buffer = new byte[bufferSize]; | ||
int bytesRead; | ||
while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) | ||
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); | ||
try | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
{ | ||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); | ||
int bytesRead; | ||
while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) | ||
{ | ||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
finally | ||
{ | ||
ArrayPool<byte>.Shared.Return(buffer); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that many of the files being edited in this change are used for CoreRT but not for CoreCLR, e.g. when compiled for coreclr, System.IO.dll type forwards Stream to the one in mscorlib. That means that these optimizations will accrue to CoreRT but not to CoreCLR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's unfortunate; is there a way around this since I believe mscorlib can't take a dependency like System.Buffers? |
||
} | ||
} | ||
|
||
|
@@ -213,11 +221,18 @@ private void InternalCopyTo(Stream destination, int bufferSize) | |
Debug.Assert(destination.CanWrite); | ||
Debug.Assert(bufferSize > 0); | ||
|
||
byte[] buffer = new byte[bufferSize]; | ||
int read; | ||
while ((read = Read(buffer, 0, buffer.Length)) != 0) | ||
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); | ||
try | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
{ | ||
int read; | ||
while ((read = Read(buffer, 0, buffer.Length)) != 0) | ||
{ | ||
destination.Write(buffer, 0, read); | ||
} | ||
} | ||
finally | ||
{ | ||
destination.Write(buffer, 0, read); | ||
ArrayPool<byte>.Shared.Return(buffer); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to return the buffer before calling dispose on the stream? This would ensure the buffers are returned even if stream.Dispose throws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this end up returning these buffers twice if it's Dispose'd twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the code should guard against double return either by checking _isDisposed, or setting _buffer to null after the first clear. Also, this should be thread-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed