关于C# Span的一些实践

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

特别感谢RC兄弟提出这个话题。

相关知识

在大多数情况下,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回收成本也不一样。

问题的产生

问个问题:写了这么多年的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做了一些优化。

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

所以,耗子尾汁。

喜欢就来个三连,让更多人因你而受益

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

问题 C: 【例2-3】围圈报数

题目描述 有&#xff4e;(n<100)个人依次围成一圈&#xff0c;从第&#xff11;个人开始报数&#xff0c;数到第&#xff4d;个人出列&#xff0c;然后从出列的下一个人开始报数&#xff0c;数到第&#xff4d;个人又出列&#xff0c;…&#xff0c;如此反复到所有的人全部…

怎样用python批量处理文件夹_python批量处理文件或文件夹

本文实例为大家分享了python批量处理文件或文件夹的具体代码&#xff0c;供大家参考&#xff0c;具体内容如下 # -*- coding: utf-8 -*- import os,shutil import sys import numpy as np ##########批量删除不同文件夹下的同名文件夹############# def arrange_file(dir_path0…

leetcode-349-两个数组的交集

给定两个数组&#xff0c;编写一个函数来计算它们的交集。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2] 示例 2&#xff1a; 输入&#xff1a;nums1 [4,9,5], nums2 [9,4,9,8,4] 输出&#xff1a;[9,4] 说明&#xff1a; 输…

Leansoft再发招贤令:面试官徐磊有话讲 | IDCF

&#xff08;图片来源于网络&#xff09;2020是Leansoft成立的第五年&#xff0c;凭借专业的服务及实施能力&#xff0c;逐渐成长为国内唯一的端到端专业DevOps实施服务公司。Leansoft是一家怎样的公司呢&#xff1f;准确地说&#xff0c;我们其实是国内唯一一家提供端到端的De…

问题 B: 数塔问题

题目描述 有如下所示的数塔&#xff0c;要求从顶层走到底层&#xff0c;若每一步只能走到相邻的结点&#xff0c;则经过的结点的数字之和最大是多少&#xff1f; 输入 第一行是一个整数N(1 < N < 20)&#xff0c;表示数塔的高度&#xff0c;接下来用N个数字表示数塔&a…

leetcode-345-翻转字符串中的元音字母

编写一个函数&#xff0c;以字符串作为输入&#xff0c;反转该字符串中的元音字母。 示例 1&#xff1a; 输入&#xff1a;“hello” 输出&#xff1a;“holle” 示例 2&#xff1a; 输入&#xff1a;“leetcode” 输出&#xff1a;“leotcede” 来源&#xff1a;力扣&…

e盾服务端源码_gRPC服务注册发现及负载均衡的实现方案与源码解析

今天聊一下gRPC的服务发现和负载均衡原理相关的话题&#xff0c;不同于Nginx、Lvs或者F5这些服务端的负载均衡策略&#xff0c;gRPC采用的是客户端实现的负载均衡。什么意思呢&#xff0c;对于使用服务端负载均衡的系统&#xff0c;客户端会首先访问负载均衡的域名/IP&#xff…

堆问题(最小堆变最大堆,堆删除,中序遍历)

2-6 设最小堆&#xff08;小根堆&#xff09;的层序遍历结果为 {8, 38, 25, 58, 52, 82, 70, 60}。用线性时间复杂度的算法将该堆调整为最大堆&#xff08;大根堆&#xff09;&#xff0c;然后连续执行两次删除最大元素操作&#xff08;DeleteMax&#xff09;。则该树的中序遍历…

推荐一款.NET Core开源爬虫神器:DotnetSpider

没有爬虫就没有互联网&#xff01;爬虫的意义在于采集大批量数据&#xff0c;然后基于此进行加工/分析&#xff0c;做更有意义的事情。谷歌&#xff0c;百度&#xff0c;今日头条&#xff0c;天眼查都离不开爬虫。去开源中国和Github查询C#的爬虫项目&#xff0c;仅有几个非常简…

Excel学习使用教程

1.Excel的保存与加密 加密&#xff1a; 我设置的密码&#xff1a;517485

python数据导入hive_Python操作HIve,将数据插入到Mysql

Python操作HIve&#xff0c;将数据插入到Mysql import sys from hive_service import ThriftHive from hive_service.ttypes import HiveServerException from thrift import Thrift from thrift.transport import TSocket from thrift.transport import TTransport from thrif…

问题 D: 二叉树求高度

题目描述 已知一棵二叉树用邻接表结构存储&#xff0c;求这棵树的高度。例&#xff1a;如图二叉树的数据文件的数据格式如下: 输入 第一行n为二叉树的结点个树&#xff0c;n≤100&#xff1b;以下第一列数据是各结点的值&#xff0c;第二列数据是左儿子结点编号&#xff0c;第…

.Net Core in Docker - 使用阿里云Codepipeline及阿里云容器镜像服务实现持续集成(CI)...

前面已经介绍过了 .Net Core In Docker 在容器内编译并发布的内容。但是每次通过 SSH 链接到服务器敲命令&#xff0c;运行脚本也是挺麻烦的一件事。程序员是最懒的&#xff0c;能让电脑解决的问题绝不手动解决&#xff0c;如果当我们push一次代码后自动build代码&#xff0c;自…

leetcode-445. 两数相加 II

给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数字都不会以零开头。 进阶&#xff1a; 如果输入链表不能修改该如何处理&#xff1f;换…

mysql 序列_MySql中序列的应用和总结

Mysql中的序列主要用于主键&#xff0c;主键是递增的字段&#xff0c;不可重复。Mysql与Oracle不同的是&#xff0c;它不支持原生态的sequence&#xff0c;需要用表和函数的组合来实现类似序列的功能。1.首先创建序列的主表/*2.其次创建如下三个函数&#xff0c;它们的功能分别…

汉诺塔问题详细解析zufeoj

汉诺塔&#xff08;Tower of Hanoi&#xff09;&#xff0c;又称河内塔&#xff0c;是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子&#xff0c;在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重…

靠刷算法题,真的可以刷进大厂吗?

我一直不知道我在大家心目中的定位是什么&#xff0c;但我内心其实是把自己定义为一个『工具人』的。可能是因为我自己本身就是程序员&#xff0c;所以更能理解程序员的不易吧。所以&#xff0c;我尽量不写水文&#xff0c;只分享干货。就是希望大家看了能够有所收获&#xff0…

PTA 7-3 地铁一日游 (30 分)

森森喜欢坐地铁。这个假期&#xff0c;他终于来到了传说中的地铁之城——魔都&#xff0c;打算好好过一把坐地铁的瘾&#xff01; 魔都地铁的计价规则是&#xff1a;起步价 2 元&#xff0c;出发站与到达站的最短距离&#xff08;即计费距离&#xff09;每 K 公里增加 1 元车费…

leetcode--912--排序数组

给你一个整数数组 nums&#xff0c;请你将该数组升序排列。 示例 1&#xff1a; 输入&#xff1a;nums [5,2,3,1] 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;nums [5,1,1,2,0,0] 输出&#xff1a;[0,0,1,1,2,5] 提示&#xff1a; 1 < nums.leng…

java 判断object类型_Java 类继承机制

封装、继承、多态是面向对象的三大特征&#xff0c;“继承”最主要的目的是为了实现代码的可复用性。通过父类与子类的继承关系&#xff0c;子类继承了父类的成员函数和成员变量&#xff0c;提高了代码的重复利用率。同时&#xff0c;子类也可以扩展自己特有的成员&#xff0c;…