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

Several Stream.Read/WriteAsync improvements #2724

Merged
merged 4 commits into from
Jan 19, 2016

Conversation

stephentoub
Copy link
Member

Stream.XxAsync are currently implemented as wrappers around Stream.Begin/EndXx. When a derived class overrides XxAsync to do its own async implementation, there's no issue (its derived XxAsync implementation will be used). When a derived class overrides Begin/EndXx but not XxAsync, there's no issue (the base implementation does what it needs to do, wrapping Begin/EndXx). However, if the derived implementation doesn't override either XxAsync or Begin/EndXx, there are a few issues.

First, there's unnecessary cost. The base Begin/EndXx methods queue a Task to call the corresponding Read/Write method. But then the XxAsync methods create another Task to wrap the Begin/End methods invocation. This means we're allocating and completing two tasks, when we only needed to do one.

Second, task wait inlining is affected. Because Read/WriteAsync aren't returning the original queued delegate-backed task but rather a promise task, Wait()'ing on the returned task blocks until the operation completes on another thread. If the original delegate-backed task were returned, then Wait()'ing on the task could potentially "inline" its execution onto the current thread.

Third, there's unnecessary blocking if there are other outstanding async operations on the instance. Since the Begin/EndXx methods were introduced, they've tracked whether an operation is in progress, and subsequent calls to BeginXx while an operation is in progress blocks synchronously (this is because most Read/Write methods on most Streams are not safe to be called concurrently). Since Read/WriteAsync just wrap the virtual Begin/End methods, they inherit this behavior.

This commit addresses all three issues for CoreCLR. The Begin/EndXx methods aren't exposed from Stream in the new contracts, and as a result outside of mscorlib we don't need to be concerned about these methods being overridden. Thus, the commit adds an optimized path that simply returns the original delegate-backed task rather than wrapping it. This avoids the unnecessary task overheads and duplication, and it enables wait inlining if someone happens to wait on it. Further, since we're no longer subject to the behaviors of Begin/End, we can change the serialization to be done asynchronously rather than synchronously. Override detection is done via a new FCall added for this purpose by @jkotas (thanks!)

On a microbenchmark that measures the overhead of Stream.XxAsync when Begin/EndXx are not overridden, this change improves single-threaded throughput by ~25% and cuts allocations by ~50%.

cc: @jkotas, @janvorli, @ericstj, @ellismg, @AlfredoMS
Related to https://github.com/dotnet/coreclr/issues/2712, https://github.com/dotnet/corefx/issues/5077

stephentoub and others added 3 commits January 19, 2016 07:18
Stream.XxAsync are currently implemented as a wrapper around Stream.Begin/EndXx.  When a derived class overrides XxAsync to do its own async implementation, there's no issue.  When a derived class overrides Begin/EndXx but not XxAsync, there's no issue (the base implementation does what it needs to do, wrapping Begin/EndXx).  However, if the derived implementation doesn't override either XxAsync or Begin/EndXx, there are a few issues.

First, there's unnecessary cost.  The base Begin/EndXx methods queue a Task to call the corresponding Read/Write method.  But then the XxAsync method create another Task to wrap the Begin/End method invocation.  This means we're allocating and completing two tasks, when we only needed to do one.

Second, task wait inlining is affected.  Because Read/WriteAsync aren't returning the original queued delegate-backed task but rather a promise task, Wait()'ing on the returned task blocks until the operation completes on another thread.  If the original delegate-backed task were returned, then Wait()'ing on the task could potentially "inline" its execution onto the current thread.

Third, there's unnecessary blocking if there are other outstanding async operations on the instance.  Since Begin/EndXx were introduced, they track whether an operation is in progress, and subsequent calls to BeginXx while an operation is in progress blocks synchronously.  Since Read/WriteAsync just wrap the virtual Begin/End methods, they inherit this behavior.

This commit addresses all three issues for CoreCLR.  The Begin/EndXx methods aren't exposed from Stream in the new contracts, and as a result outside of mscorlib we don't need to be concerned about these methods being overridden.  Thus, the commit adds an optimized path that simply returns the original delegate-backed task rather than wrapping it.  This avoids the unnecessary task overheads and duplication, and it enables wait inlining if someone happens to wait on it.  Further, since we're no longer subject to the behaviors of Begin/End, we can change the serialization to be done asynchronously rather than synchronously.
- Remove older reflection-based detection of overrides from SyncStream
- Remove Tuple allocation from RunReadWriteTaskWhenReady
- Fix spelling of Overridden
- Add a few comments
@jkotas
Copy link
Member

jkotas commented Jan 19, 2016

LGTM

@ericstj
Copy link
Member

ericstj commented Jan 19, 2016

LGTM. I'd be interested in seeing something like this on desktop. We could use similar FCalls to detect a stream has only overridden TPL methods and have the APM methods call the TPL methods rather than running the sync methods on a background thread.

jkotas added a commit that referenced this pull request Jan 19, 2016
Several Stream.Read/WriteAsync improvements
@jkotas jkotas merged commit 80c2952 into dotnet:master Jan 19, 2016
stephentoub added a commit to stephentoub/corefx that referenced this pull request Jan 19, 2016
A logical port of two of the three fixes in dotnet/coreclr#2724.  Makes it so that Read/WriteAsync calls are serialized asynchronously rather than synchronously, and so that Wait()'ing on a Read/WriteAsync task may be able to inline the execution onto the current thread.  This also reduces allocations when calling Read/WriteAsync (though there are still more optimizations that could be done).
ellismg added a commit that referenced this pull request Jan 20, 2016
@stephentoub stephentoub deleted the readwrite_perf branch January 26, 2016 03:41
if (actual == base)
return false;

if (!g_pStreamMT->IsZapped())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsZapped()? @jkotas what's the history behind that naming?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zap was the codename for ngen when it was built for .NET Framework v1.0 originally. E.g. src\zap is where most of the ngen compiler specific code lives.

picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
A logical port of two of the three fixes in dotnet/coreclr#2724.  Makes it so that Read/WriteAsync calls are serialized asynchronously rather than synchronously, and so that Wait()'ing on a Read/WriteAsync task may be able to inline the execution onto the current thread.  This also reduces allocations when calling Read/WriteAsync (though there are still more optimizations that could be done).


Commit migrated from dotnet/corefx@556f7e2
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants