Home

Awesome

where-allocation

Nearly allocation free Mono C# SendTo/ReceiveFrom NonAlloc variants.

whereallocation_smaller

Made by vis2k & FakeByte.

ReceiveFrom Allocations

Mono C#'s Socket.ReceiveFrom has heavy allocations (338 byte in Unity):

<img width="595" alt="ReceiveFrom_Before" src="https://user-images.githubusercontent.com/16416509/120093573-d24f7f80-c14d-11eb-8afe-573942b71b60.png">

Which is a huge issue for multiplayer games which try to minimize runtime allocations / GC.

It allocates because IPEndPoint .Create allocates a new IPEndPoint, and Serialize() allocates a new SocketAddress.

Both functions are called in Mono's Socket.ReceiveFrom:

int ReceiveFrom (Memory<byte> buffer, int offset, int size, SocketFlags socketFlags, ref EndPoint remoteEP, out SocketError errorCode)
{
    SocketAddress sockaddr = remoteEP.Serialize();

    int nativeError;
    int cnt;
    unsafe {
        using (var handle = buffer.Slice (offset, size).Pin ()) {
            cnt = ReceiveFrom_internal (m_Handle, (byte*)handle.Pointer, size, socketFlags, ref sockaddr, out nativeError, is_blocking);
        }
    }

    errorCode = (SocketError) nativeError;
    if (errorCode != SocketError.Success) {
        if (errorCode != SocketError.WouldBlock && errorCode != SocketError.InProgress) {
            is_connected = false;
        } else if (errorCode == SocketError.WouldBlock && is_blocking) { // This might happen when ReceiveTimeout is set
            errorCode = SocketError.TimedOut;
        }

        return 0;
    }

    is_connected = true;
    is_bound = true;

    /* If sockaddr is null then we're a connection oriented protocol and should ignore the
     * remoteEP parameter (see MSDN documentation for Socket.ReceiveFrom(...) ) */
    if (sockaddr != null) {
        /* Stupidly, EndPoint.Create() is an instance method */
        remoteEP = remoteEP.Create (sockaddr);
    }

    seed_endpoint = remoteEP;

    return cnt;
}

How where-allocation avoids the Allocations

IPEndPointNonAlloc inherits from IPEndPoint to overwrite Create(), Serialize() and GetHashCode().

Benchmarks

Using Mirror with 1000 monsters, Unity 2019 LTS (Deep Profiling), we previously allocated 8.9 KB:

<img width="702" alt="Mirror - 1k - serveronly - before" src="https://user-images.githubusercontent.com/16416509/120271597-33a65880-c2de-11eb-8f70-3dd8db20f510.png">

With where-allocation, it's reduced to 364 B:

<img width="700" alt="Mirror - 1k - serveronly - after" src="https://user-images.githubusercontent.com/16416509/120271608-399c3980-c2de-11eb-854a-51333d41b65c.png">

=> 25x reduction in allocations/GC!<br/>

Usage Guide

See the Example folder or kcp2k.

Here is how the server polls, from the Example:

if (serverSocket.Poll(0, SelectMode.SelectRead))
{
    // nonalloc ReceiveFrom
    int msgLength = serverSocket.ReceiveFrom_NonAlloc(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None, serverReusableReceiveEP);

    // new connection? then allocate an actual IPEndPoint once to store it.
    if (newClientEP == null)
        newClientEP = serverReusableReceiveEP.DeepCopyIPEndPoint();

    // process the message...
    message = new ArraySegment<byte>(receiveBuffer, 0, msgLength);
}

Tests

where-allocation comes with several unit tests to guarantee stability:

<img width="348" alt="2021-06-01_13-58-31@2x" src="https://user-images.githubusercontent.com/16416509/120273789-89c8cb00-c2e1-11eb-82b8-72a126edf128.png">

Showcase

where-allocation is used by:

Remaining Allocations

In Unity 2019/2020, Socket.ReceiveFrom_Internal still allocates 90 bytes because of the oudated Mono version:

<img width="657" alt="2021-11-30_12-17-15@2x" src="https://user-images.githubusercontent.com/16416509/144038126-c152d7fa-b9ca-4bec-bddb-3c598af030ad.png">

Unity Socket class: https://github.com/Unity-Technologies/mono/blob/unity-2021.2-mbe/mcs/class/System/System.Net.Sockets/Socket.cs

<img width="990" alt="Unity2019 LTS Mono - ReceiveFrom" src="https://user-images.githubusercontent.com/16416509/120100294-a266a300-c172-11eb-9b64-f0c04c8db0a8.png">

Unity 2021.2.0.a18 is supposed to have the latest Mono.

Which should automatically get rid of the last allocation.