3

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);
Rowan Smith
  • 1,815
  • 15
  • 29
  • Does your struct contain only [blittable](https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types) types? (Note: `string` is not blittable.) I'm assuming so, otherwise you wouldn't have the sequence of bytes in the first place... – Matthew Watson Jan 10 '20 at 09:01
  • Yes, the entire struct is blittables. Yes I am using ``Marshal.Copy`` to get a byte[] array on the other end. – Rowan Smith Jan 10 '20 at 09:06
  • You may be forced to use `byte[]` rather than `ReadOnlySequence` if you want to absolute maximum performance. Although that's what `System.IO.Pipelines` gives you, so that might not be possible. – Matthew Watson Jan 10 '20 at 09:18
  • Yes, that's right, the ``ReadOnlySequence`` comes out of the ``System.IO.Pipeline.PipeReader``, it's the only way that the buffer is provided. Sure I can copy this to a `byte[]` with the ``foreach`` loops and then do a `Marshal.Copy()` but I was looking for a clean way to do it within the API. – Rowan Smith Jan 10 '20 at 09:27

1 Answers1

4

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.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Thanks. It's negligibly slower, cleaner and most importantly doesn't create any GC pressure. – Rowan Smith Jan 10 '20 at 19:02
  • If you want to avoid the copy, then use https://learn.microsoft.com/en-us/dotnet/api/system.buffers.sequencereader-1?view=net-7.0 – davidfowl Dec 26 '22 at 22:58