-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Reduce FileStream allocations #49539
Comments
I am collecting memory allocation information for all the methods we call in the I ran the app 3 times:
Here is the spreadsheet where I am storing all the comparisons (read-only, anyone can view). I will comment on each comparison when I'm done collecting the data. |
@carlossanlop could you please include the new thanks! |
@adamsitnik Already did. Among my |
Most of the new allocations in my .NET 6.0 executions (with the FileStream Legacy code enabled or disabled), are coming from Regardless, I was able to find some large allocations that we could potentially improved in the new code with strategies (the Legacy code is not affected). The largest allocations are happening inside Both
When running Adam's rewrite with the legacy code, these were the top allocations:
When running Adam's rewrite with the refactored code, these were the top allocations:
Note:
Here is the callstack:
|
Without seeing the trace, I can't be 100% sure, but I'm 99.9% sure these are coming from the JIT, basically 1 per method for which an inlining decision is made, and it's unavoidable.
This comes from code synchronously blocking on a Task... most likely benchmark.net itself in blocking waiting for an async Task benchmark to complete.
These are the two that will be avoided by doing "use IValueTaskSource suggestion from @stephentoub". |
The top
I didn't use benchmark.net, I ran a console app and forced the consumption of the bits I compiled in my runtime repo with Adam's PR as the baseline. Here's the code for the console app. Not sure what could be causing a block, since this is what I am running: public static async Task Main()
{
File.Delete(SourceFilePath);
File.Delete(DestinationFilePath);
File.WriteAllBytes(SourceFilePath, new byte[OneMibibyte]);
File.WriteAllBytes(DestinationFilePath, new byte[OneMibibyte]);
for (int i = 1; i <= 250; i++)
{
await ReadAsync(FourKibibytes, HalfKibibyte, FileOptions.Asynchronous);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task<long> ReadAsync(
int bufferSize, // Use: 1, FourKibibytes
int userBufferSize, // Use: HalfKibibyte, FourKibibytes
FileOptions options) // Use: None, Asynchronous
{
CancellationToken cancellationToken = CancellationToken.None;
byte[] buffer = new byte[userBufferSize];
var userBuffer = new Memory<byte>(buffer);
long bytesRead = 0;
using (var fileStream = new FileStream(SourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, options))
{
while (bytesRead < OneMibibyte)
{
bytesRead += await fileStream.ReadAsync(userBuffer, cancellationToken);
}
}
return bytesRead;
}
Looking forward to fix this! |
Click the "Show Native Code" button and you'll see something very different:
Ah. I just noticed you said those are from the legacy implementation. I expect they're coming from ReadAsync itself: #16341. |
With #51363 It would be great to get rid of this allocation as well |
Presumably that's the byte[] inside of BufferedFileStreamStrategy. To hide that from such a trace, you'd need to either pool it or use native memory. How easy / hard that is will depend on how robust we want to be. We already have a semaphore gating access to async operations, so that same synchronization could be used to ensure a buffer isn't returned to a pool in Dispose while operations are erroneously in flight. Doing the same for the synchronous operations will require adding new synchronization to those code paths. |
The benchmark allocates |
I'd guess task/state machine allocations from BufferedFileStreamStrategy.WriteAsync's async method. It should show up pretty easily in a profile. |
It was #51489 |
Since we got rid of all of the allocations except of the buffer, for which we have agreed that we need a safer and more universal solution than just allowing the users to pass the buffer, I am going to close the issue. |
P0
IValueTaskSource
suggestion from @stephentoubP1
Most important is async IO (both buffering enabled and disabled) for overloads that use
ValueTask
. (@stephentoub please correct me if I am wrong)Other aspects (sync read&write, seeking, setting length, position etc) are already non-allocating.
The text was updated successfully, but these errors were encountered: