【5min+】闪电光速拳? .NetCore 中的Span
系列介绍
简介
【五分钟的DotNet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。
5min+不是超过5分钟的意思,"+"是知识的增加。so,它是让您花费5分钟以下的时间来提升您的知识储备量。
正文
在dotnet core2.x之后,引入了一个叫做Span<T>的类型。如果您的项目已经升级到了新版的dotnet core 以及使用C# 7+。您会发现我们曾经使用的许许多多类型都增加了一个扩展方法“AsSpan()”。在Vs中小手一点就会出现:
var s = ("xxx").AsSpan();
var s1 = new byte[10].AsSpan();
//.......more
那么这个家伙到底是个什么东西?怎么用呢?
先来扒一扒它的内部方法:
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();
//.....
}
这里只展示它部分的方法,但是关键的一点我们可以看到:它是一个结构性(struct 关键字)。
而且!!而且!!! 你没看错,它还加了一个ref关键字。
所以按照我们在上一篇文章中介绍过的 .net中的栈和堆,我们猜想这种结构类型的数据应该是存放在内存栈中,具有很快的访问速度。而且它拥有了ref关键字,证明它具有ref结构体的特点:
- 不能对 ref struct 装箱
- ref struct 类型不能实现接口
- 不能将 ref struct 声明为类或常规结构的字段成员
- 不能声明异步方法中属于 ref struct 类型的本地变量
- 无法在迭代器中声明 ref struct 本地变量
- 无法捕获 Lambda 表达式或本地函数中的 ref struct 变量
而且根据它公开的这些方法,我们会发现它有点类似我们常用的几个基础类型:string 、 byte[] ……。
所以直觉告诉我们,它应该是一个拿来存放数据的类型。
so,来看看MSDN - Magazine中它的解释:
System.Span<T> 是在 .NET 中发挥关键作用的新值类型。使用它,可以表示任意内存的相邻区域,无论相应内存是与托管对象相关联,还是通过互操作由本机代码提供,亦或是位于堆栈上。除了具有上述用途外,它仍能确保安全访问和高性能特性,就像数组一样。
果不其然,和我们猜想的一样。那么它出现的意义是什么呢? 性能!!!!
而且是超级快的性能。大家都知道以往如果我们想提高数据间的操作效率(比如数据偏移、裁剪等),就只能使用指针来操作内存中的数据。这样虽然一波操作猛如虎,但是写起来费劲不说,我们还得将传统的C#代码设置为不安全代码,除了添加unsafe关键字之外还需要打开项目中执行不安全代码的选项。
所以,有没有办法既不操作指针而又有高性能呢? 好吧,Span大爷来了。
Span在C# 7.x中被引入,所以它的年龄还算比较小,也是因为这些原因。以往的项目可能没有办法使用它。
它到底有多快
大家一般都是想直接看东西,所以我写了一份对比的代码。功能很简单,都是截取字符串中的一部分代码,并且进行多次的循环操作。
执行结果我都惊呆了:
是的,您没有看错。差距不是一般的大。
其实刚开始我以为Span并没有什么作用,因为我将数据源(图中的compareStr)仅仅设置为了几个单词。然后对他们进行了1亿的循环操作,但是最后的结果只有很小的差距,不到百分之30。
后来我想了一下,应该让数据更贴近现实,于是就将一张图片转换为base64然后作为数据源。结果惊呆了,差了接近百倍。而且随着循环次数和对数据源的操作次数的增多,Span和传统字符串之间的性能差距更大。
传说中的闪电光速拳到底有多快呢
它为什么这么快
它与传统的string操作比起来为什么会具有这么快的速度呢? 按照我们之前的一些猜想和msdn所给出的一点信息,我们可以得到以下的结论:
- 它分配堆栈上而不是在托管堆。
- 它所创建的数据是内存连续的,因此具有更快的遍历速度。
这些特点和string等原有类型比起来就非常的具有优势了:原来对string操作涉及到大量的字符串分配和内存复制。所以当操作的数据量小的时候还好,但是随着操作次数和处理数据量的增加之后,这是非常消耗性能的。
Span会给我们带来什么
那么,既然它拥有如此高的性能,那么我们该在什么地方使用它呢?
这很简单,如果您以前有对大量字符串进行截取或者处理的地方,一般都可以替换为Span。(为什么是一般呢😏)
除了string可以转换为span之外,其它的byte[],char[]等等都可以转换为span进行操作。所以这是非常值得高兴的一件事情,它会为我们数据处理带来显著的性能提升。比如字节流缓冲,视频流的处理,数据的加密解密等等操作都可以使用Span来完成了。
so,在现在的.NETCore runtime中,您会发现大量的类中都开始使用了Span。
而且,Span为我们实现了Explicit 和 Implicit,所以我们可以直接将支持的数组类型赋值给Span: (如果您不了解这两个关键字:戳这儿)
var arr = new byte[10];
Span<byte> bytes = arr; // 直接将byte[]赋值给Span
心动了吗?了解以下Span,并且尝试着使用它吧。
但是,请注意!! Span也是具有缺点的:因为只能存放在内存栈中,所以它不具有线程安全,它无法跨异步操作。还有它ref结构的原因,无法装箱拆箱等。
那么如果我们需要跨线程共享数据,又想拥有高性能怎么办呢? 别急,下一期咱们再来谈。😜
最后,小声说一句:创作不易,点个推荐吧😇