This repository was archived by the owner on Jan 23, 2023. It is now read-only.
Improve FileStream WriteAsync buffering and performance #2929
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
Replaces PR #2802
Fixes #1531
This set of changes improves how FileStream WriteAsync handles buffering, with some significant performance benefits. Two primary issues addressed:
The changes in this PR:
I tested this out with a variety of small benchmarks that involve writing asynchronously, e.g.
with various
arrsizes.As an example, on my laptop:
arrlength of 0x100,such a test took ~3.3 seconds and incurred ~27 gen0 GCs and ~2 gen1 GCs.In other words, for this test, this PR improved perf by ~10x. The biggest benefit comes when writing out smaller than the internal buffer size. When writing out >= the internal buffer size, throughput stayed about the same as before the change, but GCs dropped by ~6x.
The change should incur few behavioral differences. The primary observable difference is that if a WriteAsync occurs and we need to flush the buffer, we're now flushing asynchronously rather than synchronously, but because we need to ensure that Position is correct when WriteAsync returns to its synchronous caller, the subsequent asynchronous write may execute concurrently with the flush. That means that if the flush fails asynchronously, whereas before we wouldn't have issued the subsequent write and updated the position, now we will have. I believe this is a worthwhile change for the benefits.
(There are some subsequent improvements we could look at as well. For example, now that we're never allocating a secondary buffer, it becomes easier to consider using a PreAllocatedOverlapped; we would need to ensure that we only use it when there aren't concurrent writes, but that's not the common use case.)
Thanks to @tymlipari for kicking this off.
cc: @ericstj, @FiveTimesTheFun, @ianhays, @KrzysztofCwalina