Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing byte array to Span and sending it with Memory

Tags:

c#

redis

c#-7.3

I am receiving a buffer and i want from it to create a new buffer ( concatenating bytes prefixed,infixed and postfixed) and send it later on to a socket.

Eg: Initial buffer: "aaaa"
Final buffer: "$4\r\naaaa\r\n" (Redis RESP Protocol - Bulk Strings)

How can i transform the span to memory ? (I do not know if i should use stackalloc given the fact that i do not know how big the input buffer is.I figured it would be faster).

        private static readonly byte[] RESP_BULK_ID =BitConverter.GetBytes('$');
        private static readonly byte[] RESP_FOOTER = Encoding.UTF8.GetBytes("\r\n");


        static Memory<byte> GetNodeSpan(in ReadOnlyMemory<byte> payload) {

            ReadOnlySpan<byte> payloadHeader = BitConverter.GetBytes(payload.Length);

            Span<byte> result = stackalloc byte[
                                                    RESP_BULK_ID.Length +
                                                    payloadHeader.Length + 
                                                    RESP_FOOTER.Length + 
                                                    payload.Length + 
                                                    RESP_FOOTER.Length
                                                    ];
            Span<byte> cursor = result;

            RESP_BULK_ID.CopyTo(cursor);
            cursor=cursor.Slice(RESP_BULK_ID.Length);

            payloadHeader.CopyTo(cursor);
            cursor = cursor.Slice(payloadHeader.Length);

            RESP_FOOTER.CopyTo(cursor);
            cursor = cursor.Slice(RESP_FOOTER.Length);

            payload.Span.CopyTo(cursor);
            cursor = cursor.Slice(payload.Span.Length);

            RESP_FOOTER.CopyTo(cursor);

            return new Memory<byte>(result.AsBytes()) // ?can not convert from span to memory ,and cant return span because it can be referenced outside of scope
        }

P.S : Should i use old-school for loops instead of CopyTo?

like image 361
Bercovici Adrian Avatar asked May 16 '18 15:05

Bercovici Adrian


1 Answers

Memory<T> is designed to have some managed object (for example an array) as a target. Converting Memory<T> to Span<T> then simply pins target object in memory and uses it's address to construct Span<T>. But opposit conversion is not possible - because Span<T> can point to part of memory that does not belong to any managed object (unmanaged memory, stack, etc.), it is not possible to directly convert Span<T> to Memory<T>. (There is actually way to do this, but it involves implementing your own MemoryManager<T> similar to NativeMemoryManager, is unsafe and dangerous and I'm pretty sure it is not what you want).

Using stackalloc is a bad idea for two reasons:

  1. Since you don't know size of the payload in advace, you could easily get StackOverflowException if payload is too big.

  2. (as comment in your source code already suggests) It is terrible idea trying to return something allocated on the stack of current method, as it would likely result in either corrupted data or application crash.

The only way to return result on the stack would require caller of GetNodeSpan to stackalloc memory in advance, convert it to Span<T> and pass it as an additional argument. Problem is that (1) caller of GetNodeSpan would have to know how much to allocate and (2) would not help you convert Span<T> to Memory<T>.

So to store the result, you will need object allocated on the heap. The simple solution is just to allocate new array, instead of stackalloc. Such array can then be used to construct Span<T> (used for copying) as well as Memory<T> (used as a method result):

static Memory<byte> GetNodeSpan(in ReadOnlyMemory<byte> payload)
{
    ReadOnlySpan<byte> payloadHeader = BitConverter.GetBytes(payload.Length);

    byte[] result = new byte[RESP_BULK_ID.Length +
                             payloadHeader.Length +
                             RESP_FOOTER.Length +
                             payload.Length +
                             RESP_FOOTER.Length];

    Span<byte> cursor = result;

    // ...

    return new Memory<byte>(result);
}

The obvious drawback is that you have to allocate new array for each method call. To avoid this, you can use memory pooling, where allocated arrays are reused:

    static IMemoryOwner<byte> GetNodeSpan(in ReadOnlyMemory<byte> payload)
    {
        ReadOnlySpan<byte> payloadHeader = BitConverter.GetBytes(payload.Length);

        var result = MemoryPool<byte>.Shared.Rent(
                                    RESP_BULK_ID.Length +
                                    payloadHeader.Length +
                                    RESP_FOOTER.Length +
                                    payload.Length +
                                    RESP_FOOTER.Length);

        Span<byte> cursor = result.Memory.Span;

        // ...

        return result;
    }

Please note that this solution returns IMemoryOwner<byte> (instead of Memory<T>). Caller can access Memory<T> with IMemoryOwner<T>.Memory property and must call IMemoryOwner<byte>.Dispose() to return array back to pool when memory is no longer needed. Second thing to notice is that MemoryPool<byte>.Shared.Rent() can actually return array that is longer than required minimum. Thus your method will probably need to also return actual length of the result (for example as an out parameter), because IMemoryOwner<byte>.Memory.Length can return more than was actually copied to the result.

P.S.: I would expect for loop to be marginally faster only for copying very short arrays (if at all), where you can save a few CPU cycles by avoiding method call. But Span<T>.CopyTo() uses optimized method that can copy several bytes at once and (i strongly believe) uses special CPU instructions for copying blocks of memory and therefore should be much faster.

like image 105
Ňuf Avatar answered Nov 03 '22 01:11

Ňuf