c# - C# .NET Core 2.1 跨度和记忆性能注意事项
问题描述
using System.Buffers;
const byte carriageReturn = (byte)'\r';
const int arbitrarySliceStart = 5;
// using Memory<T>
async Task<int> ReadAsyncWithMemory(Stream sourceStream, int bufferSize)
{
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
var bytesRead = await sourceStream.ReadAsync(buffer);
var memory = buffer.AsMemory(arbitrarySliceStart, bytesRead);
var endOfNumberIndex = memory.Span.IndexOf(carriageReturn);
var memoryChunk = memory.Slice(0, endOfNumberIndex);
var number = BitConverter.ToInt32(memoryChunk.Span);
ArrayPool<byte>.Shared.Return(buffer);
return number;
}
// using Span<T> without assigning to variable
async Task<int> ReadAsyncWithSpan(Stream sourceStream, int bufferSize)
{
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
var bytesRead = await sourceStream.ReadAsync(buffer);
var endOfNumberIndex = buffer.AsSpan(arbitrarySliceStart, bytesRead).IndexOf(carriageReturn);
var number = BitConverter.ToInt32(buffer.AsSpan(arbitrarySliceStart, bytesRead).Slice(0, endOfNumberIndex));
ArrayPool<byte>.Shared.Return(buffer);
return number;
}
// using Span<T> with additional local or private function
async Task<int> ReadAsyncWithSpanAndAdditionalFunction(Stream sourceStream, int bufferSize)
{
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
var bytesRead = await sourceStream.ReadAsync(buffer);
var number = SliceNumer();
ArrayPool<byte>.Shared.Return(buffer);
return number;
int SliceNumer()
{
var span = buffer.AsSpan(arbitrarySliceStart, bytesRead);
var endOfNumberIndex = span.IndexOf(carriageReturn);
var numberSlice = span.Slice(0, endOfNumberIndex);
return BitConverter.ToInt32(numberSlice);
}
}
我阅读了有关 的MSDN和CodeMag文章Span<T>
,但我仍然对它们的性能有疑问。
我知道这Span<T>
比 性能更高Memory<T>
,但我想我想知道到什么程度。我发布了 3 个示例方法,我想知道哪种方法最好。
1.Memory<T>
只有
第一个函数 ,ReadAsyncWithMemory
仅用于Memory<T>
处理工作,非常简单。
2.Span<T>
没有局部变量
在第二个函数中ReadAsyncWithSpan
,Span<T>
使用 , 代替,但没有创建局部变量,并且调用buffer.AsSpan(arbitrarySliceStart, bytesRead)
了两次,这看起来很笨拙。但是,如果Span<T>
比 性能更高Memory<T>
,是否值得双重调用?
2.Span<T>
具有附加功能
在第三个函数中,ReadAsyncWithSpanAndAdditionalFunction
引入了一个局部函数,以便Span<T>
可以用于内存操作。现在的问题是,调用一个新函数并引入一个新堆栈帧是否值得使用Span<T>
over的性能提升Memory<T>
?
最后的问题
- 为跨度添加局部变量会导致额外的开销吗?
- 仅仅内联
Span<T>
而不将其分配给变量是否值得失去可读性?
- 仅仅内联
- 为了使用新函数和堆栈帧的开销而调用附加函数是否值得
Span<T>
?Memory<T>
- 与仅被限制为堆栈帧且未分配给堆时
Memory<T>
相比,性能会显着降低吗?Span<T>
解决方案
错误:您的示例中有一些错误/分心(如果编辑出问题,请删除此部分)。
AsMemory/AsSpan 采用起始索引和长度,因此
buffer.AsSpan(arbitrarySliceStart, bytesRead)
是一个错误,可能只是buffer.AsSpan(0, bytesRead)
. 如果您打算跳过读取的第一个任意SliceStart 字节,则应该buffer.AsSpan(arbitrarySliceStart, bytesRead-arbitrarySliceStart)
检查(bytesRead > arbitrarySliceStart)
.一个完整的例子,期望从一个固定的偏移量开始读取一个整数文本字段到一个流中并以回车结束,这需要一个循环来确保读取“足够”的数据(......并处理如果读取“太多”,等),但这超出了手头的主题。
这个问题似乎是关于解决编译器不允许在异步函数中使用 Span 局部变量的问题。如果 Span 变量的使用/生命周期没有交叉等待“调用”,希望未来的版本不会强制执行此限制。
- 为跨度添加局部变量会导致额外的开销吗?
不。
那么它可能会导致构成 Span 的基础指针和长度字段的额外分配/复制操作(尽管不是它们所引用的内存范围)。但即使这样也应该被优化掉,或者无论如何都可能发生在中间/临时。
这不是编译器“不喜欢” Span 变量的原因。跨度变量必须留在堆栈上,否则引用的内存可能会从它们下面收集出来,即只要它们留在堆栈上,引用内存的其他东西必须仍然在堆栈上的“下面”。异步/等待“函数”在每次等待调用时返回,然后在“等待”任务完成时作为延续/状态机调用恢复。
注意:这不仅仅是关于托管内存和 GC,否则必须检查 Spans 以获取对 GC 跟踪对象的引用。跨度可以引用非托管内存或跟踪对象块。
- 只内联 Span 而不将其分配给变量是否值得失去可读性?
嗯,这直接是一个风格/意见问题。然而,“重新创建”一个 Span 意味着一个函数调用,但没有分配(只是堆栈操作和访问/复制一些整数大小的项目);并且调用本身将是 JIT 内联的良好候选者。
- 为了使用 Span over Memory 调用附加函数是否值得新函数和堆栈帧的开销?
获得该内存将需要函数调用和堆栈帧(以及堆内存分配)。因此,这取决于您重复使用该内存的程度。而且...正常情况下,如果它没有被埋在循环中或不需要 IO,那么性能可能不是问题。
但是,请注意如何形成该额外功能。如果您关闭变量(例如在您的示例中),编译器可能会发出一个堆分配来进行该调用。
- 当内存仅限于堆栈帧且未分配给堆时,内存的性能是否明显低于 Span?
好吧,我认为您不能 stackalloc a Memory<T>
(本身),那是什么意思?
但是,与内存相比,Span 避免了对索引的一次偏移调整,因此如果您循环大量索引,则在该循环之外创建 Span 将带来好处。这可能就是为什么 Span 上提供了 IndexOf 之类的方法,而不是 Memory 的原因。
- 原始问题:哪个最好:
Memory<T>
,没有局部变量,附加函数?
这又是一个风格/意见问题(除非您实际上分析了一个表现不佳的应用程序)。
我的观点:只Span<T>
在函数边界使用 s。仅将Memory<T>
s 用于成员变量。对于“内部”代码,只需使用开始/长度或开始/结束索引变量并清楚地命名它们。与制作大量 Span/“Slices”相比,清晰的名称将有助于避免更多的错误。如果函数太长以至于不再清楚变量的含义,那么无论如何是时候考虑子函数了。