C#7.2中Span <T>和Memory <T>有什么区别?

Dan*_*sen 57 c# c#-7.2 system.memory

C#7.2引入了两个新类型:Span<T>Memory<T>有超过早先C#类型,如更好的性能string[].

问题:Span<T>和之间有什么区别Memory<T>?为什么我会使用一个而不是另一个?

Hus*_*man 53

Span<T>堆栈只能在堆栈中Memory<T>存在.

Span<T>是我们添加到平台的新类型,用于表示任意内存的连续区域,其性能特征与T []相同.它的API类似于数组,但与数组不同,它可以指向托管或本机内存,也可以指向堆栈上分配的内存.

Memory <T>是一种类型的补充Span<T>.如其设计文档中所讨论的,Span<T>是仅堆栈类型.仅堆栈特性 Span<T>使其不适用于需要Span<T>在堆上存储对缓冲区(用...表示)的引用的许多场景,例如对于执行异步调用的例程.

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;
}
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,我们将提供一组互补类型,旨在用作通用交换类型,就像Span <T>一系列任意内存一样,但不像Span <T> 这些类型不会只是堆栈,代价是重要的阅读和写入内存的性能惩罚.

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;
}
Run Code Online (Sandbox Code Playgroud)

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

参考文件:这里

  • "只有堆栈的性质"是什么意思? (7认同)
  • @Spectraljump 异步方法将转换为状态机。因此状态机的第一部分(在等待之前)被执行,它将退出该方法。当等待的任务完成时,将执行下一个状态(单独的方法),这就是第一个状态的堆栈展开的原因。 (4认同)
  • Noob在这里.不会将堆栈回滚到等待之前的位置吗?我不明白为什么缓冲区无法在`Span <byte> buffer`示例中继续存在.为什么我们将地址松散到缓冲区[0]指向的内存中? (3认同)
  • @Spectraljump如果我没记错的话,异步方法中的任何变量实际上都将是编译后的类字段(因此,在堆上),以便能够在等待后使用这些值。如果Span只能位于堆栈中,则等待之前和之后的Span都不相同。 (2认同)

小智 31

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

Span<T>可以指向任何内存:在堆栈或堆上分配.仅堆栈特性Span<T>意味着它Span<T>本身(而不是它指向的内存)必须只驻留在堆栈上.这与"普通"C#结构形成对比,后者可以驻留在堆栈上或堆上(通过值类型装箱,或者当它们嵌入在类/引用类型中时).一些更明显的实际意义是你不能Span<T>在一个类中有一个字段,你不能包含它Span<T>,你不能创建它们的数组.

  • @pep您不能将堆栈分配的缓冲区分配给“Memory”(至少不能*直接*),您可以尝试“Memory&lt;byte&gt; mem = stackalloc byte[100];”并获得编译错误 (4认同)
  • @ M.Aroosi - 对于Memory <T>来说情况不一样吗?内存<T>可以驻留在堆上,它可以指向堆栈中的内存,不是吗?这两个新手并试图理解它. (3认同)
  • 如果您可以在堆上的任何地方(通过拳击,类的成员...)获得Span &lt;T&gt;,那么堆上将有一个对象,该对象可以指向堆栈上的内存,当该对象在,例如,函数已返回。然后,您将有一个指向释放的内存的堆对象。可能导致段错误。 (2认同)

Mon*_*rso 9

一般的

Span 定义引用结构Memory定义结构

这是什么意思?

  • 引用结构不能存储在堆上,编译器将阻止您这样做,因此不允许以下操作:

    • 使用Span作为类中的字段
    • 异步方法中使用Span(异步方法正在成熟的状态机中扩展)
    • 还有更多,这里是无法使用引用结构完成的事情的完整列表。
  • stackalloc不适用于Memory(因为不能保证它不会存储在堆上),但适用于Span

    // this is legit
    Span<byte> data = stackalloc byte[256]; // legit
    
    // compile time error: Conversion of a stackalloc expression of type 'byte' to type 'Memory<byte>' is not possible.
    Memory<byte> data = stackalloc byte[256];
    
    Run Code Online (Sandbox Code Playgroud)

这意味着什么?

这意味着在某些情况下, Span本身无法进行各种微优化,因此应使用内存。

例子:

下面是一个字符串分配自由Split方法的示例,该方法适用于ReadOnlyMemory结构,在Span上实现此方法非常困难,因为Span是一个引用结构,不能放入数组IEnumerable中:

(实现摘自《C# in a nutshell 》一

IEnumerable<ReadOnlyMemory<char>> Split(ReadOnlyMemory<char> input)
{
    int wordStart = 0;
    for (int i = 0; i <= input.Length; i++)
    {
        if (i == input.Length || char.IsWhiteSpace(input.Span[i]))
        {
            yield return input.Slice(wordStart, i - wordStart);
            wordStart = i + 1;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是在 .NET SDK=6.0.403 上通过dotnet 基准测试库针对常规Split方法进行的非常简单的基准测试的结果。

|                Method |     StringUnderTest |              Mean |             Error |            StdDev |      Gen0 |      Gen1 |     Gen2 |  Allocated |
|---------------------- |-------------------- |------------------:|------------------:|------------------:|----------:|----------:|---------:|-----------:|
|          RegularSplit |                meow |         13.194 ns |         0.2891 ns |         0.3656 ns |    0.0051 |         - |        - |       32 B |
| SplitOnReadOnlyMemory |                meow |          8.991 ns |         0.1981 ns |         0.2433 ns |    0.0127 |         - |        - |       80 B |
|          RegularSplit | meow(...)meow [499] |      1,077.807 ns |        21.2291 ns |        34.8801 ns |    0.6409 |    0.0095 |        - |     4024 B |
| SplitOnReadOnlyMemory | meow(...)meow [499] |          9.036 ns |         0.2055 ns |         0.2366 ns |    0.0127 |         - |        - |       80 B |
|          RegularSplit | meo(...)eow [49999] |    121,740.719 ns |     2,221.3079 ns |     2,077.8128 ns |   63.4766 |   18.5547 |        - |   400024 B |
| SplitOnReadOnlyMemory | meo(...)eow [49999] |          9.048 ns |         0.2033 ns |         0.2782 ns |    0.0127 |         - |        - |       80 B |
|          RegularSplit | me(...)ow [4999999] | 67,502,918.403 ns | 1,252,689.2949 ns | 2,092,962.4006 ns | 5625.0000 | 2375.0000 | 750.0000 | 40000642 B |
| SplitOnReadOnlyMemory | me(...)ow [4999999] |          9.160 ns |         0.2057 ns |         0.2286 ns |    0.0127 |         - |        - |       80 B |
Run Code Online (Sandbox Code Playgroud)

这些方法的输入是“meow”字符串重复 1、100、10_000 和 1_000_000 次,我的基准设置并不理想,但它显示了差异。

  • 这个示例有点误导,因为当您使用 IEnumerable&lt;ReadOnlyMemory&lt;char&gt;&gt;` 并迭代其所有元素时,区别并没有那么大。这似乎将“string.Split”方法(执行整个字符串分割)与仅获取第一个“ReadOnlyMemory&lt;char&gt;”切片进行比较。另一个问题是一个错误:应该是:`Slice(wordStart, i - wordStart)`。 (2认同)