通俗易懂,C#如何安全、高效地玩转任何种类的内存之Span

前言

作为.net程序员,使用过指针,写过不安全代码吗?

为什么要使用指针,什么时候需要使用它?

如果能很好地回答这两个问题,那么就能很好地理解今天了主题了。C#构建了一个托管世界,在这个世界里,只要不写不安全代码,不操作指针,那么就能获得.Net至关重要的安全保障,即什么都不用担心;那如果我们需要操作的数据不在托管内存中,而是来自于非托管内存,比如位于本机内存或者堆栈上,该如何编写代码支持来自任意区域的内存呢?这个时候就需要写不安全代码,使用指针了;而如何安全、高效地操作任何类型的内存,一直都是C#的痛点,今天我们就来谈谈这个话题,讲清楚 What、How 和 Why ,让你知其然,更知其所以然,以后有人问你这个问题,就让他看这篇文章吧,呵呵。

what - 痛点是什么?

回答这个问题前,先总结一下如何用C#操作任何类型的内存:

  1. 托管内存(managed memory )

    var mangedMemory = new Student();

    很熟悉吧,只需使用new操作符就分配了一块托管内存,而且还不用手工释放它,因为它是由垃圾收集器(GC)管理的,GC会智能地决定何时释放它,这就是所谓的托管内存。默认情况下,GC通过复制内存的方式分代管理小对象(size < 85000 bytes),而专门为大对象(size >= 85000 bytes)开辟大对象堆(LOH),管理大对象时,并不会复制它,而是将其放入一个列表,提供较慢的分配和释放,而且很容易产生内存碎片。

  2. 栈内存(stack memory )

    unsafe{    
         var stackMemory = stackalloc byte[100];
    }

    很简单,使用stackalloc关键字非常快速地就分配好了一块内存,也不用手工释放,它会随着当前作用域而释放,比如方法执行结束时,就自动释放了。栈内存的容量非常小( ARM、x86 和 x64 计算机,默认堆栈大小为 1 MB),当你使用栈内存的容量大于1M时,就会报StackOverflowException 异常 ,这通常是致命的,不能被处理,而且会立即干掉整个应用程序,所以栈内存一般用于需要小内存,但是又不得不快速执行的大量短操作,比如微软使用栈内存来快速地记录ETW事件日志。

  3. 本机内存(native memory )

    通过调用方法Marshal.AllocHGlobalMarshal.AllocCoTaskMem来分配非托管内存,非托管就是垃圾回收器(GC)不可见的意思,并且还需要手工调用方法Marshal.FreeHGlobal or Marshal.FreeCoTaskMem 释放它,千万不能忘记,不然就产生内存碎片了。

抛砖引玉 - 痛点

首先我们设计一个解析完整或部分字符串为整数的API,如下

public interface IntParser{    // allows us to parse the whole string.int Parse(string managedMemory);    // allows us to parse part of the string.int Parse(string managedMemory, int startIndex, int length);    // allows us to parse characters stored on the unmanaged heap / stack.unsafe int Parse(char* pointerToUnmanagedMemory, int length);    // allows us to parse part of the characters stored on the unmanaged heap / stack.unsafe int Parse(char* pointerToUnmanagedMemory, int startIndex, int length); 
}

从上面可以看到,为了支持解析来自任何内存区域的字符串,一共写了4个重载方法。

接下来在来设计一个支持复制任何内存块的API,如下

640?wx_fmt=png

脑袋蒙圈没,以前C#操纵各种内存就是这么复杂、麻烦。通过上面的总结如何用C#操作任何类型的内存,相信大多数同学都能够很好地理解这两个类的设计,但我心里是没底的,因为使用了不安全代码和指针,这些操作是危险的、不可控的,根本无法获得.net至关重要的安全保障,并且可能还会有难以预估的问题,比如堆栈溢出、内存碎片、栈撕裂等等,微软的工程师们早就意识到了这个痛点,所以span诞生了,它就是这个痛点的解决方案

how - span如何解决这个痛点?

先来看看,如何使用span操作各种类型的内存(伪代码):

  1. 托管内存(managed memory )

    var managedMemory = new byte[100];
    Span<byte> span = managedMemory;
  2. 栈内存(stack memory )

    var stackedMemory = stackalloc byte[100];var span = new Span<byte>(stackedMemory, 100);
  3. 本机内存(native memory )

    var nativeMemory = Marshal.AllocHGlobal(100);var nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100);

span就像黑洞一样,能够吸收来自于内存任意区域的数据,实际上,现在,在.Net的世界里,Span就是所有类型内存的抽象化身,表示一段连续的内存,它的API设计和性能就像数组一样,所以我们完全可以像使用数组一样地操作各种内存,真的是太方便了。

现在重构上面的两个设计,如下:

640?wx_fmt=png

上面的方法根本不关心它操作的是哪种类型的内存,我们可以自由地从托管内存切换到本机代码,再切换到堆栈上,真正的享受玩转内存的乐趣。

why - 为什么span能解决这个痛点?

浅析span的工作机制

先来窥视一下源码:

640?wx_fmt=png

我已经圈出的三个字段:偏移量、索引、长度(使用过ArraySegment<byte> 的同学可能已经大致理解到设计的精髓了),这就是它的主要设计,当我们访问span表示的整体或部分内存时,内部的索引器会按照下面的算法运算指针(伪代码):

ref T this[int index]
{   
   get => ref ((ref reference + byteOffset) + index * sizeOf(T)); }

整个变化的过程,如图所示:

640?wx_fmt=gif

上面的动画非常清楚了吧,旧span整合它的引用和偏移成新的span的引用,整个过程并没有复制内存,而是直接返回引用,因此性能非常高,因为新span获得并更新了引用,所以垃圾回收器(GC)知道如何处理新的span,从而获得了.Net至关重要的安全保障,而这些都是span内部默默完成的,开发人员根本不用担心,非托管世界依然美好。
正是由于span的高性能,目前很多基础设施都开始支持span,甚至使用span进行重构,比如:System.String.Substring方法,我们都知道此方法是非常消耗性能的,首先会创建一个新的字符串,然后在复制原始字符串的字符集给它,而使用span可以实现Non-Allocating、Zero-coping,下面是我做的一个基准测试:

640?wx_fmt=png

使用String.SubString和Span.Slice分别截取长度为10和1000的字符串的前一半,从指标Mean可以看出方法SubString的耗时随着字符串长度呈线性增长,而Slice几乎保持不变;从指标Allocated Memory/Op可以看出,方法Slice并没有被分配新的内存,实践出真知,可以预见Span未来将会成为.Net下编写高性能应用程序的重要积木,应用前景也会非常地广,微服务、物联网都是它发光发热的好地方。

基准测试示例

总结

看完本篇博客,应该对Span的What、Why、How了如指掌了,那么我的目的就达到了,不懂的同学可以多读几遍,下一篇,我将会畅谈Span的应用场景、优缺点,让大家能够安全高效地使用好它,大家也可以在评论留言自己的应用场景,我会在写下一篇博客时多多参考。

补充

从评论区交流发现,有的同学误解了span,表面上认为只是对指针的封装,从而绕过unsafe带来的限制,避免开发人员直接面对指针而已,其实不是,下面我们来看一个示例:

var nativeMemory = Marshal.AllocHGlobal(100);
Span<byte> nativeSpan;
unsafe { nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100); } SafeSum(nativeSpan); Marshal.FreeHGlobal(nativeMemory);// 这里不关心操作的内存类型,即不用为一种类型写一个重载方法,就好比上面的设计一样。

static ulong SafeSum(Span<byte> bytes) {
 ulong sum = 0;for(int i=0; i < bytes.Length; i++) {sum += bytes[i]; }
return sum; }

看到了吗,并没有绕过unsafe,以前该如何用,现在还是一样的,span要解决的是下面两点:

  1. 高性能,避免不必要的内存分配。

  2. 高效率,不用为每一种内存类型操作写一个重载方法。

它的目标是未来将成为.Net下编写高性能应用程序的重要积木。

最后

如果有什么疑问和见解,欢迎评论区交流。
如果你觉得本篇文章对您有帮助的话,感谢您的【推荐】。
如果你对高性能编程感兴趣的话可以关注我,我会定期的在博客分享我的学习心得。
欢迎转载,请在明显位置给出出处及链接

延伸阅读

https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.cs

https://github.com/dotnet/corefxlab/blob/master/docs/specs/span.md

https://msdn.microsoft.com/en-us/magazine/mt814808

https://github.com/dotnet/BenchmarkDotNet/pull/492

https://github.com/dotnet/coreclr/issues/5851

https://adamsitnik.com/Span

做一个有底蕴的软件工程师


相关文章:

  • .Net Core中使用ref和Span<T>提高程序性能

  • C# - Span 全面介绍:探索 .NET 新增的重要组成部分

  • 有关C# 8.0、.NET Framework 4.8与NET Standard 2.1的一个说明

原文地址:https://www.cnblogs.com/justmine/p/10006621.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

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

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

相关文章

P5180-[模板]支配树

正题 题目链接:https://www.luogu.com.cn/problem/P5180 题目大意 给出nnn个点的一张有向图&#xff0c;求每个点支配的点数量。 1≤n≤2105,1≤m≤31051\leq n\leq 2\times 10^5,1\leq m\leq 3\times 10^51≤n≤2105,1≤m≤3105 解题思路 首先定义半支配点semixsemi_xsemix…

P4062 [Code+#1]Yazid 的新生舞会(区间绝对众数+分治/树状数组维护高维前缀和)

P4062 [Code#1]Yazid 的新生舞会 杭电多校懂得都懂 Code1 分治 比较喜欢分治的做法&#xff0c;非常好写。skylee大佬题解 首先对于任何一个区间来说&#xff0c;由于两个端点不确定性非常难以一次性统计多组区间&#xff0c;因为它们没有相似之处。 考虑分治&#xff0c;…

牛客题霸 [判断回文] C++题解/答案

牛客题霸 [判断回文] C题解/答案 题目描述 给定一个字符串&#xff0c;请编写一个函数判断该字符串是否回文。如果回文请返回true&#xff0c;否则返回false。 题解&#xff1a; 左右两端同时向中间缩 代码&#xff1a; class Solution { public:/*** 代码中的类名、方法…

AC自动机:例题与机制详解

介绍 AC自动机是kmp算法和trie树的结合 大体就是做这样的题用&#xff1a; 可以发现&#xff0c;这题和trie树的区别是把多个单词往一篇文章匹配&#xff0c;而trie恰好相反 匹配的时候其实就是判断子串&#xff0c;所以又用到了kmp 定义失配指针nxt[i]&#xff1a;表示root到…

.NET Core实战项目之CMS 第七章 设计篇-用户权限极简设计全过程

写在前面这篇我们对用户权限进行极简设计并保留其扩展性。首先很感谢大家的阅读&#xff0c;前面六章我带着大家快速入门了ASP.NET Core、ASP.NET Core的启动过程源码解析及配置文件的加载过程源码解析并引入依赖注入的概念、Git的快速入门、Dapper的快速入门、Vue的快速入门。…

P4428-[BJOI2018]二进制【树状数组,set】

正题 题目链接:https://www.luogu.com.cn/problem/P4428 题目大意 长度为nnn的0/10/10/1串要求支持 修改一个位置求区间[l,r][l,r][l,r]有多少个子区间重排后的二进制数可以被三整除 1≤n≤1051\leq n\leq 10^51≤n≤105 解题思路 首先有22k%31(k∈Z)2^{2k}\%31(k\in Z)22k…

2021“MINIEYE杯”中国大学生算法设计超级联赛(5)Random Walk 2(推式子+矩阵逆+矩阵乘)

Random Walk 2 【2.4】Gauss-Jordan消元法求矩阵的逆 高斯消元求矩阵的逆&#xff0c;伴随单位矩阵一起消元即可。 [A,I]→[I,A−1][\text A,\text I]\to [\text I,\text A^{-1}][A,I]→[I,A−1] 移项变形&#xff0c;后就是个矩阵的逆&#xff0c;为啥赛时不写&#xff1f;&a…

牛客题霸 [顺时针旋转矩阵] C++题解/答案

牛客题霸 [顺时针旋转矩阵] C题解/答案 题目描述 有一个NxN整数矩阵&#xff0c;请编写一个算法&#xff0c;将矩阵顺时针旋转90度。 给定一个NxN的矩阵&#xff0c;和矩阵的阶数N,请返回旋转后的NxN矩阵,保证N小于等于300。 题解&#xff1a; 根据题目要求&#xff0c;先…

数论:扩展欧几里德(洛谷P1516 青蛙的约会)

欧几里德 基本思想&#xff1a;gcd(q,r)gcd(r,q%r); 证明&#xff0c;设q、r的最大公因数为a&#xff0c;则qxa&#xff0c;rya&#xff0c;xy互质 不妨设x>y&#xff08;显然如果小于会在一次gcd运算后交换&#xff09; 则q%r&#xff08;x%y&#xff09;*a 显然,其与r的最…

CF802O-April Fools‘ Problem(hard)【wqs二分,优先队列】

正题 题目链接:https://www.luogu.com.cn/problem/CF802O 题目大意 nnn天每条有aia_iai​和bib_ibi​。 每条可以花费aia_iai​准备至多一道题&#xff0c;可以花费bib_ibi​打印至多一道准备好了的题。 求准备kkk道题最少要花费多少。 1≤k≤n≤51051\leq k\leq n\leq 5\ti…

2021牛客暑期多校训练营3 G-Yu Ling(Ling YueZheng) and Colorful Tree(cdq分治)

G-Yu Ling(Ling YueZheng) and Colorful Tree HOWARLI题解 大致做法就是首先考虑哪些修改可能影响询问&#xff0c;当修改点权是询问的倍数时才可能影响询问。于是考虑把他们放在一起。 首先每次枚举每种询问的倍数&#xff0c;把这些修改和当前询问放在一起&#xff0c;由于…

参加胶东开发者技术大会有感

2015年的时候&#xff0c;也是在12月&#xff0c;我和Bob(https://www.cnblogs.com/nianming/)去北京参加了“全球架构师峰会”&#xff0c;在那次会议上&#xff0c;来自百度、腾讯、阿里巴巴、京东、美团、新浪微博、Twitter等公司的架构师、技术专家们分享了各自在架构方面的…

牛客题霸 [矩阵乘法] C++题解/答案

牛客题霸 [矩阵乘法] C题解/答案 题目描述 给定两个nn的矩阵A和B&#xff0c;求AB。 题解&#xff1a; 都学过矩阵相乘把&#xff0c;[i][k][i][j]*[j][k] 代码&#xff1a; class Solution { public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&…

专题:数列信息传递问题转化为图论合点问题(ybtoj-数列询问+序列破解)

文章目录前言&#xff1a;一、数列询问&#xff08;取模&#xff09;解析代码二、序列破解&#xff08;奇偶性&#xff09;解析代码thanks for reading&#xff01;前言&#xff1a; 在一个数列a中&#xff0c;对于一个大区间A和组成它的两个小区间a&#xff0c;b&#xff1b;…

CF786C-Till I Collapse【树状数组倍增,优先队列】

正题 题目链接:https://www.luogu.com.cn/problem/CF786C 题目大意 给出一个长度为nnn的序列。 对于每个k∈[1,n]k\in[1,n]k∈[1,n]求将nnn分成最少的段使得每段的长度不同。 1≤ai≤n≤1051\leq a_i\leq n\leq 10^51≤ai​≤n≤105 解题思路 考虑对于一个kkk我们的做法显然…

Docker最全教程——从理论到实战(五)

在笔者参加腾讯容器服务技术交流会时&#xff0c;我们了解到了藏区牧民的目前的生活艰辛状况&#xff0c;因此除了在同事朋友之间推荐其土特产之外&#xff0c;我们也在此进行初步分享&#xff0c;希望略尽绵薄之力&#xff0c;能够帮助到他们&#xff1a;货真价实、确保都是37…

2021牛客暑期多校训练营6 J-Defend Your Country(无向图点双+思维)

无向图联通分量 点u是割点&#xff0c;当且仅当 特判树根&#xff1a;u为树根&#xff0c;且u有多于1棵子树u不为树根&#xff0c;在递归树上u存在子节点v&#xff0c;满足&#xff1a;dfnu≤lowv\text{dfn}_u\leq \text{low}_vdfnu​≤lowv​ 如上图&#xff0c;v想走到u的组…

牛客题霸 [比较版本号] C++题解/答案

牛客题霸 [比较版本号] C题解/答案 题目描述 如果version1 > version2 返回1&#xff0c;如果 version1 < version2 返回-1&#xff0c;不然返回0. 输入的version字符串非空&#xff0c;只包含数字和字符.。.字符不代表通常意义上的小数点&#xff0c;只是用来区分数字…

不止代码:循环比赛(分治)

循环比赛日程表&#xff08;match&#xff09; 【问题描述】 解析 dfs或分治 分治可以不断递归4个小正方形 左上右下为前一半&#xff0c;左下右上后一半 dfs就很无脑了 代码 #include<cstdio> #include<cstring> #include<algorithm> #include<cmath…

P4983-忘情【wqs二分,斜率优化】

正题 题目链接:https://www.luogu.com.cn/problem/P4983 题目大意 给出长度为nnn的序列xxx&#xff0c;记平均数为xˉ\bar{x}xˉ&#xff0c;要求将序列分成mmm段。 每一段[l,r][l,r][l,r]的值为 ((∑ilrxixˉ)xˉ)2xˉ2\frac{((\sum_{il}^rx_i\times \bar x)\bar x)^2}{\bar…