Skip to content

Proposal: Zero allocation connectionless sockets #30797

Description

@scalablecory

This proposal eliminates allocations for connectionless use of Socket. It augments the SocketAddress class to allow reuse across operations, becoming a high-perf alternative to EndPoint.

Rationale and Usage

APIs which need to translate between IPEndPoint and native sockaddr structures are performing a large amount of defensive copying and layering workarounds.

This affects UDP performance and contributes to excessive GC. A simple example is:

Socket socket = ...;
byte[] buffer = ...;
var remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);

socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEndPoint);
socket.SendTo(buffer, 0, buffer.Length, SocketFlags.None, remoteEndPoint);

These two calls allocate 12 times:

  • 3x IPEndPoint
  • 3x IPAddress
  • 3x SocketAddress
  • 3x byte[] ...6x if IPv6

See also: #30196

New usage has 0 allocations:

Socket socket = ...;
byte[] buffer = ...;
var remoteAddress = new SocketAddress(AddressFamily.InterNetwork);

socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, remoteAddress);
socket.SendTo(buffer, 0, buffer.Length, SocketFlags.None, remoteAddress);

Proposed API

class Socket
{
	public int ReceiveFrom(Span<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress);
	public ValueTask<int> ReceiveFromAsync(Memory<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress, CancellationToken cancellationToken = default);

	public int SendTo(ReadOnlySpan<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress);
	public ValueTask<int> SendToAsync(ReadOnlyMemory<byte> buffer, SocketFlags socketFlags, SocketAddress socketAddress, CancellationToken cancellationToken = default);
}

class SocketAsyncEventArgs
{
	// Only one of RemoteEndPoint or RemoteAddress must be specified.
	public SocketAddress RemoteAddress { get; set; }
}

class SocketAddress
{
	// If we can merge System.Net.Primitives and System.Net.Sockets, these two methods are unnecessary. That would be ideal.
	public static void GetBuffer(SocketAddress address, out byte[] buffer);
	public static void SetSockaddrSize(SocketAddress address, int size);
}

class EndPoint
{
	public virtual void SerializeTo(SocketAddress socketAddress); // Already has "SocketAddress Serialize()"; default would be to call that and copy.
}

Details

  • It is intended that UDP servers will use SocketAddress as a dictionary key to lookup client state, to avoid first converting to EndPoint.
    • It is assumed that users will only rarely care to actually get the IP/port/etc. from the SocketAddress. This duty continues to be delegated to EndPoint.
    • If users ever want to deserialize a SocketAddress into an EndPoint, they can already use EndPoint.Create.
    • Need to ensure only the actual sockaddr structure is compared/hashed, not the entire byte buffer.
  • SocketAddress is currently duplicated in System.Net.Primitives and System.Net.Sockets to avoid exposing its internal buffer. This change will allow avoiding duplication.
  • This relies on users not using a SocketAddress until the I/O is finished.
    • This is a bit safer with EndPoint as we can take defensive copies before methods return.
  • We can currently do some optimizations to avoid all allocations for SendTo and SendToAsync IPv4/IPv6 with some special casing, so this API would primarily be to optimize ReceiveFrom variants as well as (less important) allowing non-IPv4/IPv6 protocols to benefit. Still, if we were to add an API for ReceiveFrom we would probably want an API on SendTo for symmetry.

Open Questions

  • We've put some effort into not doing something like this before. It would be great to understand why. Currently:
    • We duplicate the SocketAddress class in multiple assemblies to avoid exposing its buffer, and have a step to marshal (byte-by-byte) between the two implementations.
    • Tons of APIs take EndPoint, it's a nice abstraction that we wanted here despite performance implications.
  • It isn't immediately obvious from the API surface that socketAddress is written to by ReceiveFrom. Is there a better way we can indicate this?
  • The two new methods on SocketAddress exist purely because System.Net.Sockets needs access to internals in System.Net.Primitives. Any thoughts on how to avoid exposing these "pubternal" bits?
    • One option is to merge System.Net.Primitives and System.Net.Sockets; I don't see harm in this but that is a much larger discussion :)
  • If we merge the Primitives and Sockets assemblies, we can get rid of some of the allocations for ReceiveFrom without making any API changes. It's not a perfect solution but might be good enough.

Related Issues

There are two additional issues to update our APIs with ValueTask/Span/Memory that this will need to be consistent with:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions