<T> <T> C# 7.2 中的 Span 和内存有什么区别?



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

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

Span<T>本质上只是堆栈,而Memory<T>可以存在于堆上。

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

CCD_ 9是对CCD_。如设计中所述文档,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;
}

为了解决这个问题,我们将提供一组互补类型,旨在用作通用交换类型,就像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;
}

在上面的示例中,Memory <byte>用于表示缓冲区。它是一种常规类型,可以用于执行异步操作的方法电话。其Span属性返回Span<byte>,但返回的值在异步调用期间不会存储在堆中,而是从CCD_ 18值产生新的值。在某种意义上,CCD_ 19是CCD_ 20的一个工厂。

参考文件:此处

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

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

概述

跨度定义为引用结构内存被定义为结构

这是什么意思

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

    • 使用Span作为类中的字段
    • async方法中使用Span(异步方法正在成熟的状态机中扩展)
    • 更重要的是,以下是引用结构无法完成的事情的完整列表
  • stackalloc不适用于内存(因为不能保证它不会存储在堆上),但适用于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];
    

这意味着什么

这意味着在某些情况下,Span本身不可能进行各种微观优化,因此应使用Memory

示例:

下面是一个字符串分配免费的Split方法的示例,该方法适用于ReadOnlyMemory结构,实现由于Span是一个引用结构,不能放入进入数组IEnumerable

(实现摘自C#书)

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 = i + 1;
}
}
}

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

|                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 |

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

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

最新更新