前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【5min+】传说中的孪生兄弟? Memory and Span

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

作者头像
句幽
发布2020-04-27 16:12:10
4890
发布2020-04-27 16:12:10
举报
文章被收录于专栏:一起玩转.NET一起玩转.NET

系列介绍

【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。

5min+不是超过5分钟的意思,"+"是知识的增加。so,它是让您花费5分钟以下的时间来提升您的知识储备量。

正文

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

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

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

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

什么是Memory

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

代码语言:javascript
复制
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 关键字了。

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

代码语言:javascript
复制
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

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

按照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给出来的解释,不是我乱编的哈?!(虽然和我们上面猜的一模一样(●ˇ∀ˇ●)

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

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

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

一样却又不一样

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

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

代码语言:javascript
复制
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.

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

如何使用

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

代码语言:javascript
复制
public async Task MemoryCanInLambda(Memory<string> buffer)
{
    await Task.Factory.StartNew(() =>
    {
        buffer.Trim("s");
    });
}

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

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

代码语言:javascript
复制
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,它的签名是酱紫:

代码语言:javascript
复制
public static Int32 Parse(ReadOnlySpan<char> s, NumberStyles style = NumberStyles.Integer, [NullableAttribute(2)] IFormatProvider? provider = null);

除此之外,还有longdouble…………甚至连Guid和DateTime都有这样的重载。

还有其它常用的各种类也开始支持以Span作为参数的重载方法了,比如Random、StringBuilder等。

代码语言:javascript
复制
public StringBuilder Append(ReadOnlySpan<char> value);

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

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-01-20 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 系列介绍
  • 正文
  • 一样却又不一样
  • 如何使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档