-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Two Socket/NetworkStream-related optimizations #12664
Two Socket/NetworkStream-related optimizations #12664
Conversation
Very nice 💯 |
This is great! Even better would be a way to pool the AwaitableSocketAsyncEventArgs /SocketAsyncEventArgs for reuse but that's an unrelated optimization. |
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.
Very nice!
@dotnet-bot test this please (Jenkins rebooted) |
Thanks @stephentoub. Please validate this in both netcore as well as UWP profiles using the performance tests to ensure no regressions under stress using either of the ThreadPool implementations. |
Where are instructions for doing this? |
Please manually run the outerloop https://github.com/dotnet/corefx/tree/master/src/System.Net.Sockets/tests/PerformanceTests tests, changing the counters to something that takes a couple of minutes. UWP: I'm asking only because I remember some interesting behavior differences for netcore50 as well as netnative modes. @ericeil can talk more about the differences between the two platforms in terms of IOCP/Overlapped/ThreadPool. |
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.
A few nitpicks / questions. Thanks @stephentoub!
unsafe | ||
{ | ||
Debug.Assert(_safeCloseSocket != null, "m_SafeCloseSocket is null."); | ||
Debug.Assert(SocketHandle != null, "_safeCloseSocket is null."); |
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.
Nit: SocketHandle is null.
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.
Debug.Assert(overlapped != null, "NativeOverlapped is null."); | ||
_ptrNativeOverlapped = new SafeNativeOverlapped(_currentSocket.SafeHandle, overlapped); | ||
|
||
if (_ptrNativeOverlapped != null && _ptrNativeOverlapped.SocketHandle == _currentSocket.SafeHandle) |
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.
Is there a case when _ptrNativeOverlapped.SocketHandle == _currentSocket.SafeHandle
is not true?
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.
Is there a case when _ptrNativeOverlapped.SocketHandle == _currentSocket.SafeHandle is not true?
If the same SocketAsyncEventArgs is used with one socket and then with another. I noticed we didn't have any tests for this, so I've added one as well.
@dotnet-bot Test OuterLoop Windows_NT Debug please (https://github.com/dotnet/corefx/issues/11737) |
dd54b66
to
d092bcb
Compare
Thanks. I ran the perf tests locally and didn't see any regressions. |
@dotnet-bot Test Outerloop Windows_NT Debug please |
This commit overrides CopyToAsync on NetworkStream to provide an optimized implementation. Several optimizations: - Use ArrayPool for a pooled copy buffer rather than allocating a new one for each CopyToAsync operation - Uses a SocketAsyncEventArgs to avoid per-socket operation costs like pinning of the buffer - Uses a custom awaitable to avoid per-ReadAsync costs like the allocated Tasks and IAsyncResult objects involved
…ation Each operation ends up allocating a SafeNativeOverlapped, which results in a ton of allocations when trying to optimize code via SocketAsyncEventArgs. This commit reuses the same SafeHandle object for many / all of the operations on the event args instance.
d092bcb
to
81dbf51
Compare
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
int bytesRead = await ea.ReceiveAsync(); |
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.
Should a ConfigureAwait(false)
have been added here, or was it intentionally omitted? I noticed the second await has it, but this one does not.
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.
This isn't a Task. The behavior of this awaitable matches that of ConfigureAwait(false) implicitly, in that it always ignores the current context.
I just ran into this issue over yesterday! I was seeing a GC collection happen once per-second. Glad to see it has already been fixed, awesome stuff! |
This appears to only be an issue with netcoreapp projects. Using SocketAsyncEventArgs on the full framework, 4.6, does not have this issue. I was able to create the exact same app in both frameworks and see it allocate like crazy in netcoreapp, and run nice and smooth in 4.6. Interesting, looking forward to getting this into my netcoreapp project for sure. |
…nnot attach. PerfView shows that the issue is solely dotnet/corefx#12664, so should target netstandard2.0.
…_copytoasync Two Socket/NetworkStream-related optimizations Commit migrated from dotnet/corefx@c9b1b94
This PR provides two independent but related optimizations, one for NetworkStream and one for Socket. The latter was done as it showed up as a significant source of allocations once the former was done.
I wrote a little benchmark that connects a socket to a localhost server which serves up 10MB of data. It then creates a NetworkStream and does a CopyToAsync on it to copy all of the data to Stream.Null. And it does all of this 10 times. Prior to the changes, there are lots of allocations (I'm only showing line items with > 10K of impact):

After adding CopyToAsync, most of the allocations go away, but we end up with a significant number of SafeNativeOverlapped SafeHandles:

After the second fix to address the SafeHandles, our memory usage is much more reasonable:

(Most of what remains is unrelated to the actual operation being tested, and is coming from things elsewhere in the test app, e.g. strings created at startup.)
cc: @ericeil, @davidsh, @CIPop, @davidfowl, @benaadams
Fixes #11573
Fixes #12659