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

系列介绍

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

正文

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

它与我们传统使用的基础类型相比具备超高的性能,缘由是它减小了大量的内存分配和数据量复制,而且它所分配的数据内存是连续的。github

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

x

特别是在AspNetCore中,我们会使用到大量的异步操做方法。“因此,这个时候若是咱们又想跨线程操做数据又想得到相似Span这样的性能怎么办呢?” 上一篇文章咱们留下了这样的一个问题,因此如今就是到了还愿的时候了。它就是与Span一块儿发布的孪生兄弟: Memory性能优化

x

狮子座和射手座黄金圣斗士一样具有超越光速的能力框架

什么是Memory

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

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 关键字了。异步

因此,咱们看到它的内部结构就是酱紫的:async

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属性等。ide

被声明为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

相关文章
相关标签/搜索