Span这个东西出来很久了,居然因为5.0又火起来了。

相关知识

在大多数情况下,C#开发时,我们只使用托管内存。而实际上,C#为我们提供了三种类型的内存:

  • 堆栈内存 - 最快速的内存,能够做到极快的分配和释放。堆栈内存使用时,需要用stackalloc进行分配。堆栈的一个特点是空间非常小(通常小于1 MB),适合CPU缓存。试图分配更多堆栈会报出StackOverflowException错误并终止进程;另一个特点是生命周期非常短 - 方法结束时,堆栈会与方法的内存一起释放。stackalloc通常用于必须不分配任何托管内存的短操作。一个例子是在corefx中记录快速记录ETW事件:要求尽可能快,并且需要很少的内存。
  • 非托管内存 - 通过Marshal.AllocHGlobalxMarshal.AllocCoTaskMem方法分配在非托管堆上的内存。这个内存对GC不可见,并且必须通过Marshal.FreeHGlobalMarshal.FreeCoTaskMem的显式调用来释放。使用非托管内存,最主要的目的是不给GC增加额外的压力,所以最经常的使用方式是在分配大量没有指针的值类型时使用。在Kestrel的代码中,很多地方用到了非托管内存。
  • 托管内存 - 大多数代码中最常用的内存,需要用new操作符来分配。之所以称为托管(managed),因为它是被GC(垃圾管理器)管理的,由GC决定何时释放内存,而不需要开发人员考虑。GC又将托管对象根据大小(85000字节)分为大对象和小对象。两个对象的分配方式、速度和位置都有不同,小对象相对快点,大对象相对慢点。另外,两种对象的GC回收成本也不一样。

    为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/14029853.html

问题的产生

问个问题:写了这么多年的C#,我们有用过指针吗?有没有想过为什么?

我们用个例子来回答这个问题:一个字符串,正常它是一个托管对象。

如果我们想解析整个字符串,我们会这么写:

int Parse(string managedMemory);

那么,如果我们想只解析一部分字符串,该怎么写?

int Parse(string managedMemory, int startIndex, int length);

现在,我们转到非托管内存上:

unsafe int Parse(char* pointerToUnmanagedMemory, int length);
unsafe int Parse(char* pointerToUnmanagedMemory, int startIndex, int length);

再延伸一下,我们写几个用于复制内存的功能:

void Copy<T>(T[] source, T[] destination); 
void Copy<T>(T[] source, int sourceStartIndex, T[] destination, int destinationStartIndex, int elementsCount);
unsafe void Copy<T>(void* source, void* destination, int elementsCount);
unsafe void Copy<T>(void* source, int sourceStartIndex, void* destination, int destinationStartIndex, int elementsCount);
unsafe void Copy<T>(void* source, int sourceLength, T[] destination);
unsafe void Copy<T>(void* source, int sourceStartIndex, T[] destination, int destinationStartIndex, int elementsCount);

是不是很复杂?而且看上去并不安全?

所以,问题并不在于我们能不能用,而在于这种支持会让代码变得复杂,而且并不安全 - 直到Span出现。

Span

在定义中,Span就是一个简单的值类型。它真正的价值,在于允许我们与任何类型的连续内存一起工作。

这些所谓的连续内存,包括:

  • 非托管内存缓冲区
  • 数组和子串
  • 字符串和子字符串

在使用中,Span确保了内存和数据安全,而且几乎没有开销。

使用Span

要使用Span,需要设置开发语言为C# 7.2以上,并引用System.Memory到项目。

<PropertyGroup>
  <LangVersion>7.2</LangVersion>
</PropertyGroup>

使用低版本编译器,会报错:Error CS8107 Feature 'ref structs' is not available in C# 7.0. Please use language version 7.2 or greater.

Span使用时,最简单的,可以把它想象成一个数组,它会做所有的指针运算,同时,内部又可以指向任何类型的内存。

例如,我们可以为非托管内存创建Span:

Span<byte> stackMemory = stackalloc byte[256];

IntPtr unmanagedHandle = Marshal.AllocHGlobal(256);
Span<byte> unmanaged = new Span<byte>(unmanagedHandle.ToPointer(), 256); 
Marshal.FreeHGlobal(unmanagedHandle);

T[]到Span的隐式转换:

char[] array = new char[] { 'i', 'm', 'p', 'l', 'i', 'c', 'i', 't' };
Span<char> fromArray = array;

此外,还有ReadOnlySpan,可以用来处理字符串或其他不可变类型:

ReadOnlySpan<char> fromString = "Hello world".AsSpan();

Span创建完成后,就跟普通的数组一样,有一个Length属性和一个允许读写的index,因此使用时就和一般的数组一样使用就好。

看看Span常用的一些定义、属性和方法:

Span(T[] array);
Span(T[] array, int startIndex);
Span(T[] array, int startIndex, int length);
unsafe Span(void* memory, int length);

int Length { get; }
ref T this[int index] { get; set; }

Span<T> Slice(int start);
Span<T> Slice(int start, int length);

void Clear();
void Fill(T value);

void CopyTo(Span<T> destination);
bool TryCopyTo(Span<T> destination);

我们用Span来实现一下文章开头的复制内存的功能:

int Parse(ReadOnlySpan<char> anyMemory);
int Copy<T>(ReadOnlySpan<T> source, Span<T> destination);

看看,是不是非常简单?

而且,使用Span时,运行性能极佳。关于Span的性能,网上有很多评测,关注的兄弟可以自己去看。

Span的限制

Span支持所有类型的内存,所以,它也会有相当严格的限制。

在上面的例子中,使用的是堆栈内存。所有指向堆栈的指针都不能存储在托管堆上。因为方法结束时,堆栈会被释放,指针会变成无效值,如果再使用,就是内存溢出。

因此:Span实例也不能驻留在托管堆上,而只能驻留在堆栈上。这又引出一些限制。

  1. Span不能是非堆栈类型的字段

如果在类中设置Span字段,它将被存储在堆中。这是不允许的:

class Impossible
{
    Span<byte> field;
}

不过,从C# 7.2开始,在其他仅限堆栈的类型中有Span字段是可以的:

ref struct TwoSpans<T>
{
    public Span<T> first;
    public Span<T> second;

  1. Span不能有接口实现

接口实现意味着数据会被装箱。而装箱意味着存储在堆中。同时,为了防止装箱,Span必须不实现任何现有的接口,例如最容易想到的IEnumerable。也许某一天,C#会允许定义由结构体实现的结口?

  1. Span不能是异步方法的参数

异步在C#里绝对是个好东西。

不过对于Span,是另一件事。异步方法会创建一个AsyncMethodBuilder构建器,构建器会创建一个异步状态机。异步状态机会将方法的参数放到堆上。所以,Span不能用作异步方法的参数。

  1. Span不能是泛型的代入参数

看下面的代码:

Span<byte> Allocate() => new Span<byte>(new byte[256]);

void CallAndPrint<T>(Func<T> valueProvider) 
{
    object value = valueProvider.Invoke();

    Console.WriteLine(value.ToString());
}

void Demo()
{
    Func<Span<byte>> spanProvider = Allocate;
    CallAndPrint<Span<byte>>(spanProvider);
}

同样也是装箱的原因。

上面是Span的内容。

下面简单说一下另一个经常跟Span一起提的内容:Memory

Memory

Memory是一个新的数据类型,它只能指向托管内存,所以不具有仅限堆栈的限制。

Memory可以从托管数组、字符串或IOwnedMemory中创建,传递给异步方法或存储在类的字段中。当需要Span时,就调用它的Span属性。它会根据需要创建Span。然后在当前范围内使用它。

看一下Memory的主要定义、属性和方法:

public readonly struct Memory<T>
{
    private readonly object _object;
    private readonly int _index;
    private readonly int _length;

    public Span<T> Span { get; }

    public Memory<T> Slice(int start)
    public Memory<T> Slice(int start, int length)
    public MemoryHandle Pin()
}

使用也很简单:

byte[] buffer = ArrayPool<byte>.Shared.Rent(16000 * 8);

while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
    ParseBlock(new ReadOnlyMemory<byte>(buffer, start: 0, length: bytesRead)); 
}

void ParseBlock(ReadOnlyMemory<byte> memory)
{
    ReadOnlySpan<byte> slice = memory.Span;
}

总结

Span存在很长时间了,只是5.0做了一些优化。

用好了,对代码是很好的补充和优化,用不好,就会有给自己刨很多个坑。

所以,耗子尾汁。


微信公众号:老王Plus

扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送

本文版权归作者所有,转载请保留此声明和原文链接

关于C# Span的一些实践的更多相关文章

  1. mina学习

    长连接表示一旦建立了链接,就可以长时间的保持双方的通讯,例如: socket链接,推送平台. 短链接表示建立链接,完成数据的交换之后,就断开链接,例如: http链接. mina 框架是对socket ...

  2. jQuery基础笔记(4)

    day55 参考:https://www.cnblogs.com/liwenzhou/p/8178806.html#autoid-1-9-3 文本操作 HTML代码: html()// 取得第一个匹配 ...

  3. Mina 断线重连

    Mina 断线重连 定义:这里讨论的Mina 断线重连是指使用mina作为客户端软件,连接其他提供Socket通讯服务的服务器端.Socket服务器可以是Mina提供的服务器,也可以是C++提供的服务 ...

  4. C# 8.0 添加和增强的功能【基础篇】

    .NET Core 3.x和.NET Standard 2.1支持C# 8.0. 一.Readonly 成员 可将 readonly 修饰符应用于结构的成员,来限制成员为不可修改状态.这比在C# 7. ...

  5. C# Span 源码解读和应用实践

    一:背景 1. 讲故事 这两天工作上太忙没有及时持续的文章产出,和大家说声抱歉,前几天群里一个朋友在问什么时候可以产出 Span 的下一篇,哈哈,这就来啦!读过上一篇的朋友应该都知道 Span 统一了 ...

  6. webp图片实践之路

    最近,我们在项目中实践了webp图片,并且抽离出了工具模块,整合到了项目的基础模板中.传闻IOS10也将要支持webp,那么使用webp带来的性能提升将更加明显.估计在不久的将来,webp会成为标配. ...

  7. Vuex2.0+Vue2.0构建备忘录应用实践

    一.介绍Vuex Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化,适合于构建中大型单页应用. ...

  8. [原创]django+ldap实现统一认证部分二(python-ldap实践)

    前言 接上篇文章 [原创]django+ldap实现统一认证部分一(django-auth-ldap实践) 继续实现我们的统一认证 python-ldap 我在sso项目的backend/lib/co ...

  9. 【移动前端开发实践】从无到有(统计、请求、MVC、模块化)H5开发须知

    前言 不知不觉来百度已有半年之久,这半年是996的半年,是孤军奋战的半年,是跌跌撞撞的半年,一个字:真的是累死人啦! 我所进入的团队相当于公司内部创业团队,人员基本全部是新招的,最初开发时连数据库都没 ...

随机推荐

  1. C++学习---队列的构建及操作

    一.循环队列 #include <iostream> using namespace std; #define MAXQSIZE 100 typedef struct { int* bas ...

  2. Synergy屏幕共享键鼠 (for Mac&Ubuntu)

    Synergy屏幕共享键鼠(for Mac&Ubuntu) 1.   简介 一套键盘和鼠标,操控多台电脑,下面介绍下Mac和Ubuntu之间的共享.(synergy分为服务端和客户端,把插着鼠 ...

  3. 51nod1548 欧姆诺姆和糖果

    思路: 只有兩種糖果,枚舉其中一種糖果的數量就可以得到一個可行解: 但總有一種糖果的數量是較少的,並且該數量小於sqrt(C): 簡單證明: 1.若有任一糖果的質量大於sqrt(C),則必定有一糖果的 ...

  4. 壹佰文章最全总结| 《关于ASP.NETCore的分享之路》

    学习路线图 (关于学习ASP.NET Core需要了解和掌握的知识点图) 一言不合就来图,各位博客园小伙伴大家好,感觉好久没有写文章了,自从春节开始,中间经历种种,慢慢的就开始微信公众号发文了,原因有 ...

  5. 习题解答chapter04

    题目: 实验:利用IDE的debug功能给例6.4和例6.6的new语句设置断点,使用单步调试(step into/step over)跟踪子类对象实例化(初始化)的执行顺序,并总结该过程.(教材:J ...

  6. D. Serval and Rooted Tree (樹狀DP)

    Codeforce 1153D Serval and Rooted Tree (樹狀DP) 今天我們來看看CF1153D 題目連結 題目 給一棵數,假設有$k$個葉節點,我們可以給葉節點分配$1$~$ ...

  7. 从原生web组件到框架组件源码(一)

    温馨提醒,当你觉得看我写的很乱的时候,就对了,那是因为我查阅了大量的资料提取出来的,因为有点东西不太理解,所以你会感觉有的部分重复了,也不是重复,只是后面对前面的内容进行梳理了一些,需要耐心的看到最后 ...

  8. Python + Appium 自动化操作微信入门看这一篇就够了

    简介 Appium 是一个开源的自动化测试工具,支持 Android.iOS 平台上的原生应用,支持 Java.Python.PHP 等多种语言. Appium 封装了 Selenium,能够为用户提 ...

  9. Git的介绍及使用

    一.配置用户信息: 配置用户名和邮箱: $ git config --global user.name "chunyu" $ git config --global user.em ...

  10. Java才是世界上最好的语言,Java在高频交易中替代C++

    高频交易 高频交易是指从那些人们无法利用的极为短暂的市场变化中寻求获利的计算机化交易,比如,某种证券买入价和卖出价差价的微小变化,或者某只股票在不同交易所之间的微小价差.在高频交易中,自动化应用程序每 ...