Search code examples
c#marshallingsystem.io.pipelines

A better way to copy a ReadOnlySequence<byte> to a Struct


So I have been working with System.Buffers and specifically the ReadOnlySequence<T> class lately.

I have a struct of primitives defined with the following:

[StructLayout(LayoutKind.Sequential,Pack =1,CharSet=CharSet.Unicode)]
public partial struct MessageHeader
{
    public MessageVersion Version;
    ...

And I can pass this back and forward across the network without any issues and I am leveraging System.IO.Pipelines for this.

Converting the ReadOnlySequence<byte> back into the struct has caused some head aches.

I started out with this:

    private void ExtractMessageHeaderOld(ReadOnlySequence<byte> ros, out MessageHeader messageHeader)
    {
        var x = ros.ToArray<byte>();

        ReadOnlySpan<MessageHeader> mhSpan = MemoryMarshal.Cast<byte, MessageHeader>(x);

        messageHeader = mhSpan[0];

    }

Which created thousands of little byte[] arrays over the life of the server, (which on there own aren't a problem), but with everything else the system is trying to do these put a bit of added pressure on the GC.

So I have moved to using:

    private void ExtractMessageHeader(ReadOnlySequence<byte> ros, out MessageHeader messageHeader, ref byte[] workingSpace)
    {
        var i = 0;
        foreach (var rom in ros)
            foreach (var b in rom.Span)
            {
                workingSpace[i] = b;
                i++;
            }

        ReadOnlySpan<MessageHeader> mhSpan = MemoryMarshal.Cast<byte, MessageHeader>(workingSpace);

        messageHeader = mhSpan[0];
    }

This doesn't do any memory allocations, but I just feel like there should be a better way.

The foreach loops, and the MemoryMarshal.Cast() to a messageHeader[1] so I can extract element 0 is a bit of a hack I just happened to stumble upon while reading the source.

I can't find an API to cleanly extract the contents of the ReadOnlySequence<bytes> into the messageHeader, and while what I have is working, I just feel like one should exist...

Edit 1:

I just stumbled across BuffersExtensions.CopyTo<T>(ReadOnlySequence<T>, Span<T>) which can replace the foreach loops with.

ros.CopyTo(workingSpace);

Solution

  • You could reduce the GC pressure somewhat by using stackalloc and Span<byte> to buffer the data in your first method, like so:

    public static void ExtractMessageHeaderOld(ReadOnlySequence<byte> ros, out MessageHeader messageHeader)
    {
        Span<byte> stackSpan = stackalloc byte[(int)ros.Length];
        ros.CopyTo(stackSpan);
    
        ReadOnlySpan<MessageHeader> mhSpan = MemoryMarshal.Cast<byte, MessageHeader>(stackSpan);
    
        messageHeader = mhSpan[0];
    }
    

    This may make it faster, but you'd have to instrument it to see if it really helps.