Запись байтового массива в Span и отправка его с памятью
Я получаю буфер и хочу создать из него новый буфер (объединяющий байты с префиксом, префиксом и постфиксом) и затем отправить его в сокет.
Например: начальный буфер: "aaaa"
Конечный буфер: "$4\r\naaaa\r\n"
(Redis RESP Protocol - Bulk Strings)
Как я могу преобразовать 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
}
PS: я должен использовать старую школу for
петли вместо CopyTo
?
1 ответ
Memory<T>
предназначен для использования в качестве цели некоторого управляемого объекта (например, массива). преобразование Memory<T>
в Span<T>
затем просто закрепляет целевой объект в памяти и использует его адрес для создания Span<T>
, Но противоположное преобразование невозможно - потому что Span<T>
может указывать на часть памяти, которая не принадлежит ни одному управляемому объекту (неуправляемая память, стек и т. д.), непосредственное преобразование невозможно Span<T>
в Memory<T>
, (На самом деле есть способ сделать это, но это включает в себя реализацию вашего собственного MemoryManager<T>
похож на NativeMemoryManager, небезопасен и опасен, и я уверен, что это не то, что вы хотите).
С помощью stackalloc
плохая идея по двум причинам:
Поскольку вы не знаете размер полезной нагрузки в Advace, вы можете легко получить
StackruException
если полезная нагрузка слишком велика.(как уже сказано в комментарии в вашем исходном коде) Это ужасная идея - попытаться вернуть что-то, размещенное в стеке текущего метода, так как это может привести к повреждению данных или падению приложения.
Единственный способ вернуть результат в стеке потребовал бы вызывающего 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
собственность и должна позвонить IMemoryOwner<byte>.Dispose()
вернуть массив обратно в пул, когда память больше не нужна. Второе, на что нужно обратить внимание - MemoryPool<byte>.Shared.Rent()
может на самом деле вернуть массив, который длиннее требуемого минимума. Таким образом, ваш метод, вероятно, должен будет также возвращать фактическую длину результата (например, как out
параметр), потому что IMemoryOwner<byte>.Memory.Length
может вернуть больше, чем было фактически скопировано в результат.
PS: я бы ожидал for
Цикл должен быть немного быстрее только для копирования очень коротких массивов (если вообще), где вы можете сохранить несколько циклов ЦП, избегая вызова метода. Но Span<T>.CopyTo()
использует оптимизированный метод, который может копировать несколько байтов одновременно и (я твердо верю) использует специальные инструкции процессора для копирования блоков памяти и, следовательно, должен быть намного быстрее.