C# 7.2 中的 Span<T> 和 Memory<T> 有啥区别?

Posted

技术标签:

【中文标题】C# 7.2 中的 Span<T> 和 Memory<T> 有啥区别?【英文标题】:What is the difference between Span<T> and Memory<T> in C# 7.2?C# 7.2 中的 Span<T> 和 Memory<T> 有什么区别? 【发布时间】:2022-01-14 02:50:29 【问题描述】:

C# 7.2 引入了两种新类型:Span&lt;T&gt;Memory&lt;T&gt;,它们比早期的 C# 类型(如 string[])具有更好的性能。

问题:Span&lt;T&gt;Memory&lt;T&gt; 有什么区别?为什么我要使用一个而不是另一个?

【问题讨论】:

另见:C# 7.2: Understanding Span @JeffMercado 除了 string[] 之外,还有哪些 C# 类型可以替换的完整列表吗?它们仅用于数组还是也可以用于代替 List 之类的类型? 【参考方案1】:

Span&lt;T&gt; 本质上是仅堆栈,而Memory&lt;T&gt; 可以存在于堆中。

Span&lt;T&gt; 是我们正在添加到平台以表示的新类型 任意内存的连续区域,具有性能 特性与 T[] 相当。它的API类似于数组, 但与数组不同,它可以指向托管内存或本机内存,或者 分配到堆栈上的内存。

Memory &lt;T&gt; 是对Span&lt;T&gt; 的补充类型。正如其设计中所讨论的 文档,Span&lt;T&gt; 是仅堆栈类型。仅堆栈的性质 Span&lt;T&gt; 使其不适用于很多需要存储的场景 对堆上的缓冲区(用Span&lt;T&gt; 表示)的引用,例如为了 执行异步调用的例程。

async Task DoSomethingAsync(Span<byte> buffer) 
    buffer[0] = 0;
    await Something(); // Oops! The stack unwinds here, but the buffer below
                       // cannot survive the continuation.
    buffer[0] = 1;

为了解决这个问题,我们将提供一组互补的类型, 旨在用作表示的通用交换类型, 就像Span &lt;T&gt;,一个任意内存范围,但不像Span &lt;T&gt; 这些类型不会是仅堆栈的,代价是显着的 读取和写入内存的性能损失。

async Task DoSomethingAsync(Memory<byte> buffer) 
    buffer.Span[0] = 0;
    await Something(); // The stack unwinds here, but it's OK as Memory<T> is
                       // just like any other type.
    buffer.Span[0] = 1;

在上面的示例中,Memory &lt;byte&gt; 用于表示缓冲区。 它是常规类型,可用于执行异步的方法 来电。它的 Span 属性返回Span&lt;byte&gt;,但返回值 在异步调用期间不会存储在堆上,而是 从Memory&lt;T&gt; 值生成新值。从某种意义上说, Memory&lt;T&gt;Span&lt;T&gt;的工厂。

参考文档:here

【讨论】:

“仅堆栈性质”是什么意思? 这里是菜鸟。堆栈不会回滚到等待之前的位置吗?我不明白为什么缓冲区无法在Span&lt;byte&gt; buffer 示例中继续存在。为什么我们将地址松散到缓冲区 [0] 指向的内存点? @Spectraljump 如果我没记错的话,异步方法中的任何变量实际上在编译后都会成为类字段(因此,在堆上),以便能够使用那些等待后的值。如果 Span 只能在堆栈上,那么在 await 之前和之后它就不会是相同的 Span。 @Spectraljump Async 方法将被转换为状态机。因此状态机的第一部分(在等待之前)被执行,它将退出该方法。当等待的任务完成后,将执行下一个状态(单独的方法),这就是第一个状态的堆栈展开的原因。 如果我们打算使用,为什么文档会说“以显着的性能损失为代价读取和写入内存” Memory&lt;T&gt; 在很多情况下我们不能使用Span&lt;T&gt;/ReadOnlySpan&lt;T&gt;?【参考方案2】:

re: 这意味着它只能指向分配在堆栈上的内存。

Span&lt;T&gt; 可以指向任何内存:分配在堆栈或堆上。 Span&lt;T&gt; 的仅堆栈性质意味着 Span&lt;T&gt; 本身(而不是它指向的内存)必须仅驻留在堆栈上。这与“普通”C# 结构形成对比,后者可以驻留在堆栈或堆上(通过值类型装箱,或者当它们嵌入到类/引用类型中时)。一些更明显的实际含义是,你不能在一个类中拥有 Span&lt;T&gt; 字段,你不能框 Span&lt;T&gt;,你不能将它们组成一个数组。

【讨论】:

啊,另一种类似 byref 的类型。 但是为什么只能驻留在 Stack 上呢?为什么你不能把它们装箱?还是不能让他们上课? 如果您可以在堆上的任何位置获得 Span(通过装箱、类的成员...),那么您将在堆上拥有一个可以指向内存的对象堆栈,例如,当函数返回时,堆栈将被释放。然后你会有一个指向释放内存的堆对象。这可能会导致段错误。 @M.Aroosi - Memory 的情况不一样吗? Memory 可以驻留在堆上,它可以指向堆栈中的内存,不是吗?对这两个新手并试图理解它。 @pep 你不能将堆栈分配的缓冲区分配给Memory(至少不能直接)你可以试试Memory&lt;byte&gt; mem = stackalloc byte[100]; 并得到编译错误跨度> 【参考方案3】:

Memory&lt;T&gt; 可以被视为Span&lt;T&gt; 的不安全但更通用的版本。如果Memory&lt;T&gt; 对象指向已释放的数组,则访问该对象将失败。

【讨论】:

以上是关于C# 7.2 中的 Span<T> 和 Memory<T> 有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

C# 7.1 和 7.2 Span 和 ReadOnlySpan

如何将 C# 字符串转换为 Span<char>? (跨度<T>)

C# 7.2 代理“按值返回”

在 C# 中使用 Span<T; 和 Memory<T; 编写高性能代码

使用gettext提取c#中的多语言占位符(nopCommerce示例篇)

站在前人的肩膀上重新透视C# Span<T;数据结构