C#.NET Core 2.1 Span <t>和Memory <t>性能注意事项

时间:2018-07-16 15:50:02

标签: c# .net memory memory-management .net-core

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

我阅读了有关Span<T>的{​​{3}}和MSDN文章,但是我仍然对它们的性能有疑问。

我知道Span<T>Memory<T>的性能更好,但是我想我想知道到什么程度。我发布了3个示例方法,我想知道哪种方法最好。

1。仅Memory<T>

第一个函数ReadAsyncWithMemory仅使用Memory<T>来处理工作,非常简单。

2。 Span<T>没有局部变量

在第二个函数中,改用ReadAsyncWithSpanSpan<T>,但是没有创建局部变量,并且两次调用buffer.AsSpan(arbitrarySliceStart, bytesRead),看起来很笨拙。但是,如果Span<T>Memory<T>的性能更好,是否值得重复通话?

2。 Span<T>具有其他功能

在第三个函数ReadAsyncWithSpanAndAdditionalFunction中,引入了局部函数,因此Span<T>可用于内存操作。现在的问题是,是否要调用一个新函数并引入一个新的堆栈框架,该堆栈框架是否值得在Span<T>上使用Memory<T>而获得性能提升?

最终问题

  • 为跨度添加局部变量会导致额外的开销吗?
    • 仅内联Span<T>而不将其分配给变量是否值得失去可读性?
  • 是否要调用一个附加函数,以便在Span<T>上使用Memory<T>而不值得新函数和堆栈框架的开销?
  • Memory<T>仅限于堆栈帧而未分配给堆的情况下,它们的性能是否显着低于Span<T>

1 个答案:

答案 0 :(得分:1)

错误::示例中存在一些错误/干扰(如果从问题中进行编辑,请删除此部分)。

  1. AsMemory / AsSpan具有起始索引和长度,因此buffer.AsSpan(arbitrarySliceStart, bytesRead) 是一个错误,可能仅为buffer.AsSpan(0, bytesRead)。如果您打算跳过读取的第一个randomSliceStart字节,则应该是buffer.AsSpan(arbitrarySliceStart, bytesRead-arbitrarySliceStart)并可能需要检查(bytesRead < arbitrarySliceStart)

  2. 一个完整的示例希望将整数文本字段读入流中的固定偏移量并以回车符终止,因此需要一个循环以确保读取“足够”的数据(...如果“太多”),但这不在手头的话题之外。

这个问题似乎与解决编译器问题有关,该编译器禁止异步函数中的Span局部变量。希望如果Span变量的使用/生存期没有超过等待的“调用次数”,将来的版本将不会强制执行此限制。

  • 为跨度添加局部变量会导致额外的开销吗?

否。

好吧可能会对构成Span的基础指针和长度字段进行额外的赋值/复制操作(尽管不是它们所引用的内存范围)。但是,即使应该进行优化,也可以或仅通过中间/临时版本进行

这不是为什么编译器“不喜欢” Span变量的原因。跨度变量必须保留在堆栈上,否则引用的内存可能会从它们下面收集出来,即,只要它们保留在堆栈上,引用内存的某些东西必须仍然在堆栈“下方”。异步/等待“功能”在每次等待调用时返回,然后在“等待”任务完成时作为继续/状态机调用恢复。

注意:这不仅与托管内存有关,而且还必须由GC检查Span以获取对GC跟踪对象的引用。跨区可以引用非托管内存或跟踪的对象块。

  • 仅通过内联Span而不将其分配给变量是否值得失去可读性?

好吧,这直接是样式/意见的问题。但是,“重新创建” Span意味着调用函数但没有分配(只需堆栈操作和访问/复制一些整数大小的项目);该电话本身将是进行JIT内联的一个很好的选择。

  • 是否要调用附加函数以使用“跨越内存”(Span over Memory)功能值得新功能和堆栈框架的开销?

要获得该内存,将需要一个函数调用和堆栈帧(以及堆内存分配)。因此,这取决于您有多少重用该内存。而且...如果不将其埋在循环中或不需要IO,则正常情况下性能可能不是问题。

如何,请注意如何形成该额外功能。如果关闭变量(如您的示例中所示),则编译器可能会发出堆分配以进行该调用。

  • 仅将其限制为堆栈帧而不分配给堆时,内存的性能是否显着低于Span?

好吧,我不认为您可以自己分配Memory<T>,这是什么意思?

但是,与内存相比,Span避免了对索引的一次偏移调整,因此,如果您遍历很多索引,那么在该循环之外创建Span将会带来很多好处。这可能是为什么在Span上提供了诸如IndexOf之类的方法,但在Memory上却没有提供的原因。

  • 原始问题:哪个最好:Memory<T>,没有语言环境变量,还有其他功能?

同样,这是一个样式/意见问题(除非您实际描述了性能不佳的应用程序)。

我的看法:仅在函数边界使用Span<T>。仅将Memory<T>用作成员变量。对于“内部”代码,只需使用开始/长度或开始/结束索引变量并清楚地命名它们即可。清晰的名称将有助于避免产生大量Span /“切片”的错误。如果函数太长了,以至于无法弄清变量的含义,那么该是时候将其分解为子函数了。