-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FileStream rewrite: Caching the ValueTaskSource in AsyncWindowsFileStreamStrategy #51363
Conversation
Tagging subscribers to this area: @carlossanlop Issue DetailsFixes #50972 DAFT: Pending benchmarks. When This PR is a continuation of #50802, in which we switched from using Changes:
|
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
Initial benchmark results (base is #50802):
it looks like we are allocating less when buffering is disabled, but more than before when it's enabled |
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.
Overall looks good to me, but we need to track and solve the allocation regression which is visible in the scenarios where buffering is enabled
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
…to the moment after _source.SetException|SetResult is called
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
@stephentoub your suggestions were great!
|
@stephentoub I believe I have addressed all your feedback, PTAL one more time. I hope that we can merge it today and include it in Preview 4 |
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
...m.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs
Outdated
Show resolved
Hide resolved
@stephentoub we have addressed the feedback, please take a look. I am going to post the benchmark results in 20-30 minutes |
This now also fixes #25074 |
The results (see the
|
Wowza some of those allocation improvements are incredible! |
Nice. |
@@ -1067,7 +1069,8 @@ private void EnsureBufferAllocated() | |||
|
|||
void AllocateBuffer() // logic kept in a separate method to get EnsureBufferAllocated() inlined | |||
{ | |||
_strategy.OnBufferAllocated(_buffer = new byte[_bufferSize]); | |||
_buffer = GC.AllocateUninitializedArray<byte>(_bufferSize, | |||
pinned: true); // this allows us to avoid pinning when the buffer is used for the syscalls |
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.
That correct, but pinned: true
also allocates the array in Gen2 as side-effect so this may actually hurt real-world scenarios at the end..
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.
We can allocate it and use a GCHandle if that ends up being better. Previously it was pinned as part of a PreallocatedOverlapped.
(It's still not at all obvious when this newfangled POH should be used. )
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.
We can also investigate not pinning at all, in which case the code this interacts with will just create a gchandle for each operation.
And/or look at using a pool array, but we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use. We do that in a few other streams.
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.
Here is a simple test:
using System;
using System.IO;
for (int i = 0; i < 100_000; i++)
{
using (var f = new FileStream("test", FileMode.Create))
{
f.WriteByte(42);
}
}
Console.WriteLine($"Allocated: {GC.GetTotalAllocatedBytes()} Gen2 GCs: {GC.CollectionCount(2)}");
- .NET 5: Allocated: 442474096 Gen2 GCs: 0
- This PR: Allocated: 448051624 Gen2 GCs: 103
It will be interesting to see whether these excessive Gen2 GCs hit performance gates of services trying .NET 6 previews.
It's still not at all obvious when this newfangled POH should be used.
Agree. It is very hard to use.
we'd want to ensure enough synchronization was in place to minimize erroneous usage causing us to return an array still in use
If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.
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.
So allocating on the POH contributes to the gen2 budget. This is why we disable the buffer using size 1, that still works right?
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.
Nothing in this file is used at all if buffer size is 1.
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.
I'm looking forward to taking another stab at optimizing static files in .NET 6
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.
If you can cover all these cases, it may be better to use unmanaged buffer. It is pinned too, and it does not cause excessive Gen2 GCs.
I'm going to start with a GCHandle and a normally allocated array. I believe in that case I can mostly restrict synchronization to the async code paths (plus disposal). If we use a native buffer, we'll need to protect the sync code paths as well. We can start with this and then see if it makes sense to use a pooled or native buffer as well.
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.
Well, actually, I'm going to start by not pinning here at all (it'll then pin/unpin in the rest of the implementation per operation). If there's no measurable impact, we can stick with that for now.
Fixes #50972
Fixes #25074
When
AsyncWindowsFileStreamStrategy
is wrapped by aBufferedFileStreamStrategy
, we need to make sure theValueTaskSource
instance is cached to reduce the number of allocations when callingReadAsync
orWriteAsync
multiple times in a row.This PR is a continuation of #50802, in which we switched from using
TaskCompletionSource
toIValueTaskSource
.Changes:
PreAllocatedOverlapped
instance insideValueTaskSource
, so the latter becomes its owner. This was done because we are only supposed to have an instance of aPreAllocatedOverlapped
if theValueTaskSource
was created fromOnBufferedAllocated
, which is a method called only byBufferedFileStreamStrategy
right before writing or reading.MemoryValueTaskSource
and moved the cases handled by it toValueTaskSource
.NativeOverlapped*
. This is done every time we callReadAsync
/WriteAsync
, to make sure we are pinning the memory passed by the user.