Sockets on Windows: reduce array allocations during Select, Poll, Receive, Send (#30485)

* untested; zero allocs during Socket.Poll and Socket.Select on Windows (aveat: Socket.Select with > cutoff will still allocate)

* use MemoryMarshal.GetReference - cleanly passes null ref for empty span

* remove all the "unsafe"; pretty sure that the "ref" will count as a "fixed" during the P/Invoke

* avoid allocating in Socket.Send/Socket.Receive when passing multiple segments (synchronous API)

* Revert "avoid allocating in Socket.Send/Socket.Receive when passing multiple segments (synchronous API)"

This reverts commit 343b88602bd7974f64ae8247f5415b6bf590a89b.

* use spans for multi-segment sync send/receive

* remove "unsafe" from select - no longer required

* fix nit whitespace

* address @stephentoub feedback from review:

- prefer Foo* to ref Foo in p/invoke
- avoid repeated .Count access
- use const size stackalloc instead of dynamic
- use ArrayPool instead of allocate
- avoid multiple testing of count when determining stack vs heap
- use smaller stack threshold

* add debug assertions to express intent of file descriptor size vs socket list size

* use slice+span.clear to simplify objectsToPin wipe
This commit is contained in:
Marc Gravell 2018-06-18 16:56:46 +01:00 коммит произвёл Stephen Toub
Родитель 30148bf4a5
Коммит 146fe58389
5 изменённых файлов: 176 добавлений и 97 удалений

Просмотреть файл

@ -40,15 +40,15 @@ internal static partial class Interop
internal static unsafe SocketError WSARecv(
IntPtr socketHandle,
WSABuffer[] buffers,
Span<WSABuffer> buffers,
int bufferCount,
out int bytesTransferred,
ref SocketFlags socketFlags,
NativeOverlapped* overlapped,
IntPtr completionRoutine)
{
Debug.Assert(buffers != null && buffers.Length > 0 );
fixed (WSABuffer* buffersPtr = &buffers[0])
Debug.Assert(!buffers.IsEmpty);
fixed (WSABuffer* buffersPtr = &MemoryMarshal.GetReference(buffers))
{
return WSARecv(socketHandle, buffersPtr, bufferCount, out bytesTransferred, ref socketFlags, overlapped, completionRoutine);
}

Просмотреть файл

@ -40,15 +40,15 @@ internal static partial class Interop
internal static unsafe SocketError WSASend(
IntPtr socketHandle,
WSABuffer[] buffers,
Span<WSABuffer> buffers,
int bufferCount,
out int bytesTransferred,
SocketFlags socketFlags,
NativeOverlapped* overlapped,
IntPtr completionRoutine)
{
Debug.Assert(buffers != null && buffers.Length > 0);
fixed (WSABuffer* buffersPtr = &buffers[0])
Debug.Assert(!buffers.IsEmpty);
fixed (WSABuffer* buffersPtr = &MemoryMarshal.GetReference(buffers))
{
return WSASend(socketHandle, buffersPtr, bufferCount, out bytesTransferred, socketFlags, overlapped, completionRoutine);
}

Просмотреть файл

@ -10,19 +10,19 @@ internal static partial class Interop
internal static partial class Winsock
{
[DllImport(Interop.Libraries.Ws2_32, SetLastError = true)]
internal static extern int select(
internal static extern unsafe int select(
[In] int ignoredParameter,
[In, Out] IntPtr[] readfds,
[In, Out] IntPtr[] writefds,
[In, Out] IntPtr[] exceptfds,
[In] IntPtr* readfds,
[In] IntPtr* writefds,
[In] IntPtr* exceptfds,
[In] ref TimeValue timeout);
[DllImport(Interop.Libraries.Ws2_32, SetLastError = true)]
internal static extern int select(
internal static extern unsafe int select(
[In] int ignoredParameter,
[In, Out] IntPtr[] readfds,
[In, Out] IntPtr[] writefds,
[In, Out] IntPtr[] exceptfds,
[In] IntPtr* readfds,
[In] IntPtr* writefds,
[In] IntPtr* exceptfds,
[In] IntPtr nullTimeout);
}
}

Просмотреть файл

@ -4,6 +4,7 @@
using Microsoft.Win32.SafeHandles;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
@ -122,16 +123,18 @@ namespace System.Net.Sockets
return transmitPackets(socketHandle, packetArray, elementCount, sendSize, overlapped, flags);
}
internal static IntPtr[] SocketListToFileDescriptorSet(IList socketList)
internal static void SocketListToFileDescriptorSet(IList socketList, Span<IntPtr> fileDescriptorSet)
{
if (socketList == null || socketList.Count == 0)
int count;
if (socketList == null || (count = socketList.Count) == 0)
{
return null;
return;
}
IntPtr[] fileDescriptorSet = new IntPtr[socketList.Count + 1];
fileDescriptorSet[0] = (IntPtr)socketList.Count;
for (int current = 0; current < socketList.Count; current++)
Debug.Assert(fileDescriptorSet.Length >= count + 1);
fileDescriptorSet[0] = (IntPtr)count;
for (int current = 0; current < count; current++)
{
if (!(socketList[current] is Socket))
{
@ -140,24 +143,27 @@ namespace System.Net.Sockets
fileDescriptorSet[current + 1] = ((Socket)socketList[current])._handle.DangerousGetHandle();
}
return fileDescriptorSet;
}
// Transform the list socketList such that the only sockets left are those
// with a file descriptor contained in the array "fileDescriptorArray".
internal static void SelectFileDescriptor(IList socketList, IntPtr[] fileDescriptorSet)
internal static void SelectFileDescriptor(IList socketList, Span<IntPtr> fileDescriptorSet)
{
// Walk the list in order.
//
// Note that the counter is not necessarily incremented at each step;
// when the socket is removed, advancing occurs automatically as the
// other elements are shifted down.
if (socketList == null || socketList.Count == 0)
int count;
if (socketList == null || (count = socketList.Count) == 0)
{
return;
}
if ((int)fileDescriptorSet[0] == 0)
Debug.Assert(fileDescriptorSet.Length >= count + 1);
int returnedCount = (int)fileDescriptorSet[0];
if (returnedCount == 0)
{
// No socket present, will never find any socket, remove them all.
socketList.Clear();
@ -166,13 +172,13 @@ namespace System.Net.Sockets
lock (socketList)
{
for (int currentSocket = 0; currentSocket < socketList.Count; currentSocket++)
for (int currentSocket = 0; currentSocket < count; currentSocket++)
{
Socket socket = socketList[currentSocket] as Socket;
// Look for the file descriptor in the array.
int currentFileDescriptor;
for (currentFileDescriptor = 0; currentFileDescriptor < (int)fileDescriptorSet[0]; currentFileDescriptor++)
for (currentFileDescriptor = 0; currentFileDescriptor < returnedCount; currentFileDescriptor++)
{
if (fileDescriptorSet[currentFileDescriptor + 1] == socket._handle.DangerousGetHandle())
{
@ -180,10 +186,11 @@ namespace System.Net.Sockets
}
}
if (currentFileDescriptor == (int)fileDescriptorSet[0])
if (currentFileDescriptor == returnedCount)
{
// Descriptor not found: remove the current socket and start again.
socketList.RemoveAt(currentSocket--);
count--;
}
}
}

Просмотреть файл

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using Microsoft.Win32.SafeHandles;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
@ -116,16 +117,31 @@ namespace System.Net.Sockets
IntPtr.Zero);
return errorCode == SocketError.SocketError ? GetLastSocketError() : SocketError.Success;
}
public static SocketError Send(SafeCloseSocket handle, IList<ArraySegment<byte>> buffers, SocketFlags socketFlags, out int bytesTransferred)
{
const int StackThreshold = 16; // arbitrary limit to avoid too much space on stack (note: may be over-sized, that's OK - length passed separately)
int count = buffers.Count;
WSABuffer[] WSABuffers = new WSABuffer[count];
GCHandle[] objectsToPin = null;
bool useStack = count <= StackThreshold;
WSABuffer[] leasedWSA = null;
GCHandle[] leasedGC = null;
Span<WSABuffer> WSABuffers = stackalloc WSABuffer[0];
Span<GCHandle> objectsToPin = stackalloc GCHandle[0];
if (useStack)
{
WSABuffers = stackalloc WSABuffer[StackThreshold];
objectsToPin = stackalloc GCHandle[StackThreshold];
}
else
{
WSABuffers = leasedWSA = ArrayPool<WSABuffer>.Shared.Rent(count);
objectsToPin = leasedGC = ArrayPool<GCHandle>.Shared.Rent(count);
}
objectsToPin = objectsToPin.Slice(0, count);
objectsToPin.Clear(); // note: touched in finally
try
{
objectsToPin = new GCHandle[count];
for (int i = 0; i < count; ++i)
{
ArraySegment<byte> buffer = buffers[i];
@ -156,16 +172,18 @@ namespace System.Net.Sockets
}
finally
{
if (objectsToPin != null)
for (int i = 0; i < count; ++i)
{
for (int i = 0; i < objectsToPin.Length; ++i)
if (objectsToPin[i].IsAllocated)
{
if (objectsToPin[i].IsAllocated)
{
objectsToPin[i].Free();
}
objectsToPin[i].Free();
}
}
if (!useStack)
{
ArrayPool<WSABuffer>.Shared.Return(leasedWSA);
ArrayPool<GCHandle>.Shared.Return(leasedGC);
}
}
}
@ -239,13 +257,29 @@ namespace System.Net.Sockets
public static SocketError Receive(SafeCloseSocket handle, IList<ArraySegment<byte>> buffers, ref SocketFlags socketFlags, out int bytesTransferred)
{
const int StackThreshold = 16; // arbitrary limit to avoid too much space on stack (note: may be over-sized, that's OK - length passed separately)
int count = buffers.Count;
WSABuffer[] WSABuffers = new WSABuffer[count];
GCHandle[] objectsToPin = null;
bool useStack = count <= StackThreshold;
WSABuffer[] leasedWSA = null;
GCHandle[] leasedGC = null;
Span<WSABuffer> WSABuffers = stackalloc WSABuffer[0];
Span<GCHandle> objectsToPin = stackalloc GCHandle[0];
if (useStack)
{
WSABuffers = stackalloc WSABuffer[StackThreshold];
objectsToPin = stackalloc GCHandle[StackThreshold];
}
else
{
WSABuffers = leasedWSA = ArrayPool<WSABuffer>.Shared.Rent(count);
objectsToPin = leasedGC = ArrayPool<GCHandle>.Shared.Rent(count);
}
objectsToPin = objectsToPin.Slice(0, count);
objectsToPin.Clear(); // note: touched in finally
try
{
objectsToPin = new GCHandle[count];
for (int i = 0; i < count; ++i)
{
ArraySegment<byte> buffer = buffers[i];
@ -276,16 +310,18 @@ namespace System.Net.Sockets
}
finally
{
if (objectsToPin != null)
for (int i = 0; i < count; ++i)
{
for (int i = 0; i < objectsToPin.Length; ++i)
if (objectsToPin[i].IsAllocated)
{
if (objectsToPin[i].IsAllocated)
{
objectsToPin[i].Free();
}
objectsToPin[i].Free();
}
}
if (!useStack)
{
ArrayPool<WSABuffer>.Shared.Return(leasedWSA);
ArrayPool<GCHandle>.Shared.Return(leasedGC);
}
}
}
@ -668,10 +704,10 @@ namespace System.Net.Sockets
return SocketError.Success;
}
public static SocketError Poll(SafeCloseSocket handle, int microseconds, SelectMode mode, out bool status)
public static unsafe SocketError Poll(SafeCloseSocket handle, int microseconds, SelectMode mode, out bool status)
{
IntPtr rawHandle = handle.DangerousGetHandle();
IntPtr[] fileDescriptorSet = new IntPtr[2] { (IntPtr)1, rawHandle };
IntPtr* fileDescriptorSet = stackalloc IntPtr[2] { (IntPtr)1, rawHandle };
Interop.Winsock.TimeValue IOwait = new Interop.Winsock.TimeValue();
// A negative timeout value implies an indefinite wait.
@ -708,61 +744,97 @@ namespace System.Net.Sockets
return SocketError.Success;
}
public static SocketError Select(IList checkRead, IList checkWrite, IList checkError, int microseconds)
public static unsafe SocketError Select(IList checkRead, IList checkWrite, IList checkError, int microseconds)
{
IntPtr[] readfileDescriptorSet = Socket.SocketListToFileDescriptorSet(checkRead);
IntPtr[] writefileDescriptorSet = Socket.SocketListToFileDescriptorSet(checkWrite);
IntPtr[] errfileDescriptorSet = Socket.SocketListToFileDescriptorSet(checkError);
// This code used to erroneously pass a non-null timeval structure containing zeroes
// to select() when the caller specified (-1) for the microseconds parameter. That
// caused select to actually have a *zero* timeout instead of an infinite timeout
// turning the operation into a non-blocking poll.
//
// Now we pass a null timeval struct when microseconds is (-1).
//
// Negative microsecond values that weren't exactly (-1) were originally successfully
// converted to a timeval struct containing unsigned non-zero integers. This code
// retains that behavior so that any app working around the original bug with,
// for example, (-2) specified for microseconds, will continue to get the same behavior.
int socketCount;
if (microseconds != -1)
const int StackThreshold = 64; // arbitrary limit to avoid too much space on stack
bool ShouldStackAlloc(IList list, ref IntPtr[] lease, out Span<IntPtr> span)
{
Interop.Winsock.TimeValue IOwait = new Interop.Winsock.TimeValue();
MicrosecondsToTimeValue((long)(uint)microseconds, ref IOwait);
socketCount =
Interop.Winsock.select(
0, // ignored value
readfileDescriptorSet,
writefileDescriptorSet,
errfileDescriptorSet,
ref IOwait);
}
else
{
socketCount =
Interop.Winsock.select(
0, // ignored value
readfileDescriptorSet,
writefileDescriptorSet,
errfileDescriptorSet,
IntPtr.Zero);
int count;
if (list == null || (count = list.Count) == 0)
{
span = default;
return false;
}
if (count >= StackThreshold) // note on >= : the first element is reserved for internal length
{
span = lease = ArrayPool<IntPtr>.Shared.Rent(count + 1);
return false;
}
span = default;
return true;
}
if (NetEventSource.IsEnabled) NetEventSource.Info(null, $"Interop.Winsock.select returns socketCount:{socketCount}");
if ((SocketError)socketCount == SocketError.SocketError)
IntPtr[] leaseRead = null, leaseWrite = null, leaseError = null;
try
{
return GetLastSocketError();
Span<IntPtr> readfileDescriptorSet = ShouldStackAlloc(checkRead, ref leaseRead, out var tmp) ? stackalloc IntPtr[StackThreshold] : tmp;
Socket.SocketListToFileDescriptorSet(checkRead, readfileDescriptorSet);
Span<IntPtr> writefileDescriptorSet = ShouldStackAlloc(checkWrite, ref leaseWrite, out tmp) ? stackalloc IntPtr[StackThreshold] : tmp;
Socket.SocketListToFileDescriptorSet(checkWrite, writefileDescriptorSet);
Span<IntPtr> errfileDescriptorSet = ShouldStackAlloc(checkError, ref leaseError, out tmp) ? stackalloc IntPtr[StackThreshold] : tmp;
Socket.SocketListToFileDescriptorSet(checkError, errfileDescriptorSet);
// This code used to erroneously pass a non-null timeval structure containing zeroes
// to select() when the caller specified (-1) for the microseconds parameter. That
// caused select to actually have a *zero* timeout instead of an infinite timeout
// turning the operation into a non-blocking poll.
//
// Now we pass a null timeval struct when microseconds is (-1).
//
// Negative microsecond values that weren't exactly (-1) were originally successfully
// converted to a timeval struct containing unsigned non-zero integers. This code
// retains that behavior so that any app working around the original bug with,
// for example, (-2) specified for microseconds, will continue to get the same behavior.
int socketCount;
fixed (IntPtr* readPtr = &MemoryMarshal.GetReference(readfileDescriptorSet))
fixed (IntPtr* writePtr = &MemoryMarshal.GetReference(writefileDescriptorSet))
fixed (IntPtr* errPtr = &MemoryMarshal.GetReference(errfileDescriptorSet))
{
if (microseconds != -1)
{
Interop.Winsock.TimeValue IOwait = new Interop.Winsock.TimeValue();
MicrosecondsToTimeValue((long)(uint)microseconds, ref IOwait);
socketCount =
Interop.Winsock.select(
0, // ignored value
readPtr,
writePtr,
errPtr,
ref IOwait);
}
else
{
socketCount =
Interop.Winsock.select(
0, // ignored value
readPtr,
writePtr,
errPtr,
IntPtr.Zero);
}
}
if (NetEventSource.IsEnabled)
NetEventSource.Info(null, $"Interop.Winsock.select returns socketCount:{socketCount}");
if ((SocketError)socketCount == SocketError.SocketError)
{
return GetLastSocketError();
}
Socket.SelectFileDescriptor(checkRead, readfileDescriptorSet);
Socket.SelectFileDescriptor(checkWrite, writefileDescriptorSet);
Socket.SelectFileDescriptor(checkError, errfileDescriptorSet);
return SocketError.Success;
}
finally
{
if (leaseRead != null) ArrayPool<IntPtr>.Shared.Return(leaseRead);
if (leaseWrite != null) ArrayPool<IntPtr>.Shared.Return(leaseWrite);
if (leaseError != null) ArrayPool<IntPtr>.Shared.Return(leaseError);
}
Socket.SelectFileDescriptor(checkRead, readfileDescriptorSet);
Socket.SelectFileDescriptor(checkWrite, writefileDescriptorSet);
Socket.SelectFileDescriptor(checkError, errfileDescriptorSet);
return SocketError.Success;
}
public static SocketError Shutdown(SafeCloseSocket handle, bool isConnected, bool isDisconnected, SocketShutdown how)