【问题标题】:C# .NET Core 2.1 Span<T> and Memory<T> Performance ConsiderationsC# .NET Core 2.1 Span<T> 和 Memory<T> 性能注意事项
【发布时间】:2018-12-24 05:31:48
【问题描述】:
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&lt;T&gt;MSDNCodeMag 文章,但我仍然对它们的性能有疑问。

我知道Span&lt;T&gt;Memory&lt;T&gt; 性能更高,但我想我想知道到什么程度。我发布了 3 个示例方法,我想知道哪种方法最好。

1.仅限Memory&lt;T&gt;

第一个函数ReadAsyncWithMemory,只使用Memory&lt;T&gt;来处理工作,非常简单。

2。 Span&lt;T&gt; 没有局部变量

在第二个函数中,改用ReadAsyncWithSpanSpan&lt;T&gt;,但是没有创建局部变量,并且调用了两次buffer.AsSpan(arbitrarySliceStart, bytesRead),看起来很笨拙。但是,如果 Span&lt;T&gt;Memory&lt;T&gt; 性能更高,是否值得双重调用?

2。 Span&lt;T&gt; 有附加功能

在第三个函数ReadAsyncWithSpanAndAdditionalFunction中,引入了一个局部函数,以便Span&lt;T&gt;可以用于内存操作。现在的问题是,调用一个新函数并引入一个新堆栈帧是否值得使用Span&lt;T&gt; 而不是Memory&lt;T&gt; 带来的性能提升?

最后的问题

  • 为跨度添加局部变量会导致额外开销吗?
    • 只内联 Span&lt;T&gt; 而不将其分配给变量是否值得失去可读性?
  • 为了使用Span&lt;T&gt; 而不是Memory&lt;T&gt; 调用附加函数是否值得新函数和堆栈帧的开销?
  • Memory&lt;T&gt; 仅被限制为堆栈帧而不分配给堆时,其性能是否明显低于Span&lt;T&gt;

【问题讨论】:

  • 您是否尝试过使用 benchmarkDotNet 或类似工具对此进行基准测试?
  • 我不介意这样做,但我想听听一些解释,说明为什么一个比另一个更好。我想这三个的表现也很相似。
  • buffer.AsSpan(arbitrarySliceStart, bytesRead).Slice(0, endOfNumberIndex)buffer.AsSpan(arbitrarySliceStart, endOfNumberIndex) 相同。

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


【解决方案1】:

错误:您的示例中存在一些错误/干扰(如果编辑不妥,请删除此部分)。

  1. AsMemory/AsSpan 采用起始索引和长度,所以buffer.AsSpan(arbitrarySliceStart, bytesRead) 是一个错误,可能只是buffer.AsSpan(0, bytesRead)。如果您打算跳过读取的第一个任意SliceStart 字节,它应该是buffer.AsSpan(arbitrarySliceStart, bytesRead-arbitrarySliceStart),可能需要检查(bytesRead &gt; arbitrarySliceStart)

  2. 一个完整的例子,希望从一个固定的偏移量开始读取一个整数文本字段到一个流中并以一个回车符结束,这需要一个循环来确保读取“足够”的数据(......并处理 if “读了太多”等),但这超出了当前的主题。

这个问题似乎是关于解决编译器不允许在异步函数中使用 Span 局部变量的问题。如果 Span 变量的使用/生命周期没有交叉等待“调用”,希望未来的版本不会强制执行此限制。

  • 为 span 添加局部变量是否会导致额外开销?

没有。

嗯,它可能导致对构成 Span 的底层指针和长度字段进行额外的赋值/复制操作(尽管 不是它们所引用的内存范围)。但即使这样也应该被优化掉或者无论如何都可能发生在中间/临时

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

注意:这不仅仅是关于托管内存和 GC,否则 GC 必须检查 Spans 以查找对 GC 跟踪对象的引用。 Spans 可以引用非托管内存或跟踪对象块。

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

嗯,这直接是一个风格/意见问题。然而,“重新创建”一个 Span 意味着一个函数调用,但没有分配(只是堆栈操作和访问/复制一些整数大小的项目);并且调用本身将是 JIT 内联的良好候选者。

  • 为了使用 Span over Memory 调用额外的函数是否值得新函数和堆栈帧的开销?

要获得该内存将需要函数调用和堆栈帧(以及堆内存分配)。因此,这取决于您重复使用该内存的程度。而且...正常情况下,如果它没有埋在循环中或不需要 IO,那么性能可能不是问题。

但是,要小心你如何形成这个额外的功能。如果您关闭变量(如您的示例中所示),编译器可能会发出一个堆分配来进行该调用。

  • 当 Memory 被限制为仅堆栈帧而不分配给堆时,它的性能是否明显低于 Span?

好吧,我认为您不能 stackalloc 一个 Memory&lt;T&gt;(本身),那么这是什么意思?

但是,与内存相比,Span 避免了对索引的一次偏移调整,因此如果您循环执行大量索引,则在该循环之外创建 Span 将带来好处。这可能就是 Span 提供了 IndexOf 之类的方法,而不提供 Memory 的原因。

  • 原问题:哪个最好:Memory&lt;T&gt;,没有局部变量,附加函数?

这又是一个风格/意见问题(除非您实际分析了一个表现不佳的应用程序)。

我的意见:只在函数边界处使用Span&lt;T&gt;s。仅将Memory&lt;T&gt;s 用于成员变量。对于“内部”代码,只需使用开始/长度或开始/结束索引变量并清楚地命名它们。与制作大量 Span/“Slices”相比,清晰的名称将有助于避免更多的错误。如果函数太长以至于不再清楚变量的含义,那么无论如何是时候将其分解为子函数了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多