【5min+】传说中的孪生兄弟? Memory and Span

系列介绍

【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。
5min+不是超过5分钟的意思,"+"是知识的增加。so,它是让您花费5分钟以下的时间来提升您的知识储备量。

正文

在上一篇文章:《闪电光速拳? .NetCore 中的Span》 中我们提到了在.net core 2.x 所新增的一个类型:Span

它与咱们传统使用的基础类型相比具有超高的性能,原因是它减少了大量的内存分配和数据量复制,并且它所分配的数据内存是连续的。

但是您会发现它无法用在我们项目的某些地方,它独特的 ref结构 使它没有办法跨线程使用、更没有办法使用Lambda表达式。

x

特别是在AspNetCore中,咱们会使用到大量的异步操作方法。“所以,这个时候如果我们又想跨线程操作数据又想获得类似Span这样的性能怎么办呢?” 上一篇文章我们留下了这样的一个问题,所以现在就是到了还愿的时候了。它就是与Span一起发布的孪生兄弟: Memory

x

狮子座和射手座黄金圣斗士同样具备超越光速的能力

什么是Memory

那什么是Memory呢?不妨我们先来猜测一下,它的结构是什么样子。毕竟它是Span的孪生兄弟,而Span的结构我们在前面就了解过了:

public readonly ref struct Span<T>
{
    public void Clear();
    public void CopyTo([NullableAttribute(new[] { 0, 1 })] Span<T> destination);
    public void Fill(T value);
    public Enumerator GetEnumerator();
    public Span<T> Slice(int start, int length);
    public T[] ToArray();
    public override string ToString();

    //.....
}

当时我们说Span有各种缺陷的原因是由于它独特的 ref struct 关键字所导致的,导致它无法拆箱装箱、无法书写Lambda、无法跨线程等。但是它兄弟却可以克服缺点,所以我们想想它会和Span在声明上有哪些差距呢? 是的,您可能已经想到了:它不会有 ref 关键字了。

所以,我们看到它的内部结构就是酱紫的:

public readonly struct Memory<T>
{
    public static Memory<T> Empty { get; }
    public bool IsEmpty { get; }
    public int Length { get; }
    public Span<T> Span { get; }
    public void CopyTo([NullableAttribute(new[] { 0, 1 })] Memory<T> destination);
    public MemoryHandle Pin();
    public Memory<T> Slice(int start, int length);
    public T[] ToArray();
    public override string ToString();
}

和我们猜想的一样。它少了ref关键字,内部方法也和Span差不多(同样拥有CopyTo,Slice等),但是还是有一些差异,比如多了Pin方法,Span属性等。

被声明为ref struct的结构,叫做“ByRefLike”。所以在我们在进行反射的时候,我们使用Type会看到有这样一个属性:IsByRefLike

x

好像有点超纲了哈(>人<;)

按照MSDN给出的解释:

该结构是使用中的C# ref struct 关键字声明的。 不能将类似 byref 的结构的实例放置在托管堆上。

所以这也是为什么上一篇文章说的:Span只能放置在内存栈中的原因。

那么反过来想,没有了ref关键字之后。Memory是不是就可以放置在托管堆上了呢?是不是就可以进行拆装箱,克隆副本供其它线程的内存栈使用了呢? 好吧,可能是这样。所以这也许就是它能够被允许跨线程使用的原因吧。

进行到了这一步,那我们再回过头来想想Memory是什么呢? 其实现在我们心里其实都已经有个底了:

与 Span<T>一样,Memory<T> 表示内存的连续区域。 但 Span<T>不同,Memory<T> 不是ref 结构。 这意味着 Memory<T> 可以放置在托管堆上,而 Span<T> 不能。 因此,Memory<T> 结构与 Span<T> 实例没有相同的限制。 具体而言:

  • 它可用作类中的字段。
  • 它可跨 await 和 yield 边界使用。

除了 Memory<T>之外,还可以使用 System.ReadOnlyMemory<T> 来表示不可变或只读内存。

这是MSDN给出来的解释,不是我乱编的哈😝!(虽然和我们上面猜的一模一样(●ˇ∀ˇ●)

接下来,我们来看看他们到底有多像:

x

好吧,为了做该图我已经使用了美工必杀器 - ps😭

有没有发现,除了名字之外,好像其它的都一模一样😱。甚至直接连注释都懒得改了。

一样却又不一样

既然作为孪生兄弟,必然有一些共通之处。而Memory作为对Span的增强(应该也算不算增强吧),那么内部的实现可能很多会与Span相似。

是的,查看Memory的源代码您就会发现,它的内部某些方法就是通过Span来实现的:

public readonly struct Memory<T>
{
    public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span);
    public T[] ToArray() => Span.ToArray();
}

有关Memory的源代码,您可以点此查看:the source code of Memory

所以您会发现Memory是可以直接转换为Span的。但是Memory作为一个可以跨线程的类型被转换为Span是相对危险的,所以Dotnet Core的开发人员直接在备注上写了这样的文字:

Such a cast can only be done with unsafe or marshaling code,in which case that's the dangerous operation performed by the dev, and we're just following suit here to make it work as best as possible.

意思就是这种转换很危险,我来帮你做了算了。

x

如何使用

来吧,修改上面的Span会在Task中报错的例子:

public async Task MemoryCanInLambda(Memory<string> buffer)
{
    await Task.Factory.StartNew(() =>
    {
        buffer.Trim("s");
    });
}

此时我们就可以在异步中使用Memory了,采用连续内存+指针级别的操作方案来操作数据内容,岂不爽歪歪?

异步的数据交由Memory,同步的数据交由Span,ForExample:


static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream)
{
  int bytesRead = await stream.ReadAsync(buffer);
  return Checksum(buffer.Span.Slice(0, bytesRead));
  // Or buffer.Slice(0, bytesRead).Span
}
static int Checksum(Span<byte> buffer) { ... }

正是由于SpanMemory带来的巨大性能优化,所以.NET Core的开发者们做了一件非常疯狂的事:为.NET的库添加了数百个重载方法。 比如,您现在可以看到我们经常使用的Int.Parse方法居然支持了Span,它的签名是酱紫:

public static Int32 Parse(ReadOnlySpan<char> s, NumberStyles style = NumberStyles.Integer, [NullableAttribute(2)] IFormatProvider? provider = null);

除此之外,还有longdouble…………甚至连Guid和DateTime都有这样的重载。
还有其它常用的各种类也开始支持以Span作为参数的重载方法了,比如Random、StringBuilder等。

public StringBuilder Append(ReadOnlySpan<char> value);

先不谈重建这些基础常用类型的重载工作量有多大,我们应该想想.NET为什么要这么做呢?就是为了我们能够使用SpanMemory来代替我们现有的一些操作,从而提升性能。

那么仅仅是开发底层框架才适合用它们吗? 当然不是,就好比是截取字符串的操作,无论是底层框架还是应用程序级别的代码都会用到。所以如果有可能,而当我们的项目又正好是.netCore 2.x以上的版本,为何不去尝试使用下呢?

不要因为“我知道Span不过就是把原有的某某操作放到内存某处,不过如此”,就对它产生偏见。确实,Span的实现很简单,您如果有兴趣可以查看它的实现代码。.net core正在为它的实现和使用做巨大的适配工作,C# 从7.x 开始就不断对异步操作和内存分配进行优化,这或许也为我们未来.NET的发展给了一点点提示。加油,伟大的开发人员们。(ง •_•)ง

最后,小声说一句:创作不易,点个推荐吧😇

x

posted @ 2020-01-20 18:13  句幽  阅读(2066)  评论(17编辑  收藏