将字节数组写入 Span 并用 Memory 发送
Writing byte array to Span and sending it with Memory
我正在接收一个缓冲区,我想从中创建一个新缓冲区(连接带前缀、中缀和后缀的字节)并稍后将其发送到套接字。
例如:
初始缓冲区:"aaaa"
最终缓冲区:"\r\naaaa\r\n"
(Redis RESP 协议 - 批量字符串)
如何将 span
转换为 memory
? (我不知道我是否应该使用 stackalloc
,因为我不知道输入有多大 buffer
is.I 认为它会更快)。
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
是个坏主意,原因有二:
因为你事先不知道payload的大小,你可以很容易地得到WhosebugException
if payload太大了。
(正如源代码中的注释已经暗示的那样)尝试 return 在当前方法的堆栈上分配一些东西是个糟糕的主意,因为它可能会导致数据损坏或应用程序崩溃。
将return结果压入堆栈的唯一方法是需要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;
}
请注意,此解决方案 returns IMemoryOwner<byte>
(而不是 Memory<T>
)。调用者可以使用 IMemoryOwner<T>.Memory
属性 访问 Memory<T>
并且当不再需要内存时必须调用 IMemoryOwner<byte>.Dispose()
到 return 数组返回池。要注意的第二件事是 MemoryPool<byte>.Shared.Rent()
实际上可以 return 比要求的最小值更长的数组。因此,您的方法可能还需要 return 结果的实际长度(例如作为 out
参数),因为 IMemoryOwner<byte>.Memory.Length
可以 return 比实际复制到结果。
P.S.: 我希望 for
循环仅在复制非常短的数组(如果有的话)时稍微快一些,您可以通过以下方式节省一些 CPU 循环避免方法调用。但是 Span<T>.CopyTo()
使用可以一次复制多个字节的优化方法,并且(我坚信)使用特殊的 CPU 指令来复制内存块,因此应该更快。
我正在接收一个缓冲区,我想从中创建一个新缓冲区(连接带前缀、中缀和后缀的字节)并稍后将其发送到套接字。
例如:
初始缓冲区:"aaaa"
最终缓冲区:"\r\naaaa\r\n"
(Redis RESP 协议 - 批量字符串)
如何将 span
转换为 memory
? (我不知道我是否应该使用 stackalloc
,因为我不知道输入有多大 buffer
is.I 认为它会更快)。
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
是个坏主意,原因有二:
因为你事先不知道payload的大小,你可以很容易地得到
WhosebugException
if payload太大了。(正如源代码中的注释已经暗示的那样)尝试 return 在当前方法的堆栈上分配一些东西是个糟糕的主意,因为它可能会导致数据损坏或应用程序崩溃。
将return结果压入堆栈的唯一方法是需要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;
}
请注意,此解决方案 returns IMemoryOwner<byte>
(而不是 Memory<T>
)。调用者可以使用 IMemoryOwner<T>.Memory
属性 访问 Memory<T>
并且当不再需要内存时必须调用 IMemoryOwner<byte>.Dispose()
到 return 数组返回池。要注意的第二件事是 MemoryPool<byte>.Shared.Rent()
实际上可以 return 比要求的最小值更长的数组。因此,您的方法可能还需要 return 结果的实际长度(例如作为 out
参数),因为 IMemoryOwner<byte>.Memory.Length
可以 return 比实际复制到结果。
P.S.: 我希望 for
循环仅在复制非常短的数组(如果有的话)时稍微快一些,您可以通过以下方式节省一些 CPU 循环避免方法调用。但是 Span<T>.CopyTo()
使用可以一次复制多个字节的优化方法,并且(我坚信)使用特殊的 CPU 指令来复制内存块,因此应该更快。