c# - 将字节数组写入 Span 并用 Memory 发送

标签 c# redis c#-7.3

我正在接收一个缓冲区,我想从中创建一个新缓冲区(连接带前缀、中缀和后缀的字节)并稍后将其发送到套接字。

例如: 初始缓冲区:"aaaa"
最终缓冲区:"$4\r\naaaa\r\n"(Redis RESP 协议(protocol) - 批量字符串)

如何将 span 转换为 memory ? (我不知道我是否应该使用 stackalloc,因为我不知道输入 buffer 有多大。我认为它会更快)。

        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:我应该使用老式的 for 循环而不是 CopyTo 吗?

最佳答案

Memory<T>旨在将某些托管对象(例如数组)作为目标。转换 Memory<T>Span<T>然后简单地将目标对象固定在内存中并使用它的地址来构造 Span<T> .但是相反的转换是不可能的 - 因为 Span<T>可以指向不属于任何托管对象的部分内存(非托管内存,堆栈等),不能直接转换Span<T>Memory<T> . (实际上有办法做到这一点,但它涉及实现你自己的 MemoryManager<T> 类似于 NativeMemoryManager ,是不安全和危险的,我很确定这不是你想要的)。

使用 stackalloc是个坏主意,原因有二:

  1. 由于您不知道 advace 中的有效负载的大小,您可以轻松获得 StackOverflowException如果有效负载太大。

  2. (正如您的源代码中的注释所暗示的那样)尝试返回在当前方法的堆栈上分配的内容是一个糟糕的想法,因为它可能会导致数据损坏或应用程序崩溃。

在堆栈上返回结果的唯一方法是调用 GetNodeSpan 的调用者至 stackalloc提前内存,转换成Span<T>并将其作为附加参数传递。问题是 (1) GetNodeSpan 的来电者必须知道要分配多少,并且 (2) 不会帮助您转换 Span<T>Memory<T> .

因此要存储结果,您需要在堆上分配对象。简单的解决方案就是分配新数组,而不是 stackalloc .然后可以使用这样的数组来构造 Span<T> (用于复制)以及 Memory<T> (用作方法结果):

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);
}

明显的缺点是您必须为每个方法调用分配新数组。为避免这种情况,您可以使用内存池,在其中重用分配的数组:

    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;
    }

请注意,此解决方案返回 IMemoryOwner<byte> (而不是 Memory<T> )。来电者可以访问Memory<T>IMemoryOwner<T>.Memory属性(property),必须调用 IMemoryOwner<byte>.Dispose()当不再需要内存时将数组返回到池中。第二件事要注意的是 MemoryPool<byte>.Shared.Rent()实际上可以返回比要求的最小值更长的数组。因此,您的方法可能还需要返回结果的实际长度(例如作为 out 参数),因为 IMemoryOwner<byte>.Memory.Length可以返回比实际复制到结果更多的内容。

P.S.:我希望 for仅在复制非常短的数组(如果有的话)时循环稍微快一些,您可以通过避免方法调用来节省几个 CPU 周期。但是Span<T>.CopyTo()使用可以一次复制多个字节的优化方法,并且(我坚信)使用特殊的 CPU 指令来复制内存块,因此应该更快。

关于c# - 将字节数组写入 Span 并用 Memory 发送,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50374782/

相关文章:

c# - IBM MQ 客户端向后兼容性

c# - 如果元素计数小于 'n',则删除数组中的所有相似元素

C# 以编程方式访问 Excel 宏

c# - 结构指针(地址)和默认构造函数

c# 7.3 ValueTuple - 在解构后检查默认值

c# - WCF 中的调用上下文

node.js - 如何实现 Laravel、node.js、socket.io 和 redis 以使用数据库创建实时聊天/通知

javascript - 在具有许多连接的数据库中快速分配游戏管理

scala - 在 Akka 中,如何将响应从下游参与者路由到正确的上游?

c# - 为什么包含 ValueTuple 的结构可以满足非托管约束,而 ValueTuple 本身却不能?