这两天工做上太忙没有及时持续的文章产出,和你们说声抱歉,前几天群里一个朋友在问何时能够产出 Span 的下一篇,哈哈,这就来啦!读过上一篇的朋友应该都知道 Span 统一了 .NET 程序 栈 + 托管 + 非托管
实现了三大块内存的统一访问,🐂👃,并且在 .net 底层 Library 中也是一等公民的存在,不少现有的类都提供了对 Span / ReadOnlySpan 的支持。git
public sealed class String { [MethodImpl(MethodImplOptions.InternalCall)] [NullableContext(0)] public extern String(ReadOnlySpan<char> value); }
public sealed class StringBuilder : ISerializable { public unsafe StringBuilder Append(ReadOnlySpan<char> value) { if (value.Length > 0) { fixed (char* value2 = &MemoryMarshal.GetReference(value)) { Append(value2, value.Length); } } return this; } }
public readonly struct Int32 { public static int Parse(ReadOnlySpan<char> s, NumberStyles style = NumberStyles.Integer, IFormatProvider? provider = null) { NumberFormatInfo.ValidateParseStyleInteger(style); return Number.ParseInt32(s, style, NumberFormatInfo.GetInstance(provider)); } }
怎么样,这些通用 & 基础的类都在大力对接 Span / ReadOnlySpan
,更别说复杂类型了,其地位不言自明哈,接下来咱们就从 Span 自己的机制聊起。github
灵活运用 Span 解决工做中的实际问题我相信你们应该没什么毛病了,有了这个基础再从 Span 的源码 和 用户态 和你们一块儿深度剖析,从源码开始吧。数组
public readonly ref struct Span<T> { internal readonly ByReference<T> _pointer; private readonly int _length; }
上面代码的 ref struct
能够看出,这个 Span 是只能够分配在栈上的值类型,而后就是里面的 _pointer 和 _length 两个实例字段,不知道看完这两个字段脑子里是否是有一幅图,大概是这样的。安全
能够清晰的看出,Span 就是用来映射一段能够连续访问的内存地址,空间大小由 length 控制,开始位置由 _pointer 指定,是否是像极了指针😁😁😁,是的,语言团队要保证你的程序高性能,还得照护你的人身安全,出了各类手段,真是煞费苦心! 👍👍👍框架
虽然图已经画了,但仍是有不少朋友但愿眼见为实,必须实操演练,嘿嘿,无惧任何挑战,那我先把上面的图化成代码:ide
static void Main(string[] args) { var nums = new int[] { 1, 2, 3, 4, 5, 6 }; var span = new Span<int>(nums); Console.ReadLine(); }
接下来我用 windbg 把线程栈中的 span 也找出来。源码分析
0:000> !clrstack -l OS Thread Id: 0x181c (0) Child SP IP Call Site 000000963277E5D0 00007ffc3e601434 ConsoleApp1.Program.Main(System.String[]) [E:\net5\ConsoleApp2\ConsoleApp1\Program.cs @ 13] LOCALS: 0x000000963277E618 = 0x000001e956b8ab10 0x000000963277E608 = 0x000001e956b8ab20
从最后一行代码能够看出:span 的栈地址是 0x000000963277E608,栈内容是:0x000001e956b8ab20,按照图的理论: 0x000001e956b8ab20 应该是 nums 数组元素 1 的内存地址,能够用 dp 验证一下。性能
0:000> dp 0x000001e956b8ab20 000001e9`56b8ab20 00000002`00000001 00000004`00000003 000001e9`56b8ab30 00000006`00000005 00000000`00000000 000001e9`56b8ab40 00007ffc`3e6c4388 00000000`00000000
从上面三行内存地址来看,数组的:1,2,3,4,5,6
依次排列,有些朋友可能有点小疑问,为啥 nums 的内存地址不是指向数组元素 1 的呢? 那我来普及一下吧,先用 dp 唤出数组的内存地址。ui
0:000> dp 0x000001e956b8ab10 000001e9`56b8ab10 00007ffc`3e69f090 00000000`00000006 000001e9`56b8ab20 00000002`00000001 00000004`00000003 000001e9`56b8ab30 00000006`00000005 00000000`00000000
能够看出,第一排为: 00007ffc3e69f090 0000000000000006
, 前面的 8 byte 表示 数组 的 方法表地址,后面的 8byte 表示 6 ,也就是说数组有 6个元素,不信的话我截一张图:this
span 是由 _pointer + length 组成的,刚才的 _pointer 也给你们演示了,那 length 的值在哪里呢? 由于 span 是 struct,因此须要用 dp 把刚才的线程栈最小的栈地址打出来就能够了。
到这里,我以为我讲的已经够清楚了,若是还有点懵的话能够仔细想想哈。
Span的应用场景真的是太多了,不可能在这篇一一列举,这里我就举两个例子吧,让你们可以感觉到 Span 的强大便可。
案例:如何高效的计算出用户输入的值 10+20
?
传统的作法很简单,截取呗,代码以下:
static void Main(string[] args) { var word = "10+20"; var splitIndex = word.IndexOf("+"); var num1 = int.Parse(word.Substring(0, splitIndex)); var num2 = int.Parse(word.Substring(splitIndex + 1)); var sum = num1 + num2; Console.WriteLine($"{num1}+{num2}={sum}"); Console.ReadLine(); }
结果是很轻松的算出来了,但你仔细想一想这里是否是有点什么问题,好比说为了从 word 中扣出 num,我用了两次 SubString,就意味着会在 托管堆 上生成两个 string,若是说我执行 1w 次话,那托管堆上会不会有 2w 个 string 呢? 修改代码以下:
for (int i = 0; i < 10000; i++) { var num1 = int.Parse(word.Substring(0, splitIndex)); var num2 = int.Parse(word.Substring(splitIndex + 1)); var sum = num1 + num2; }
而后看一下 托管堆 上 String 的个数
0:000> !dumpheap -type String -stat Statistics: MT Count TotalSize Class Name 00007ffc53a81e18 20167 556538 System.String
托管堆上有 20167 个,挺恐怖的,真的是给 GC 添麻烦哈,这里还有 167 个是系统自带的,接下来的问题是有没有办法替换 SubString 从而不生成临时string呢?
若是看懂了 Span 结构图,你就应该会使用 _pointer + length 将 string 进行切片处理,对不对,代码以下:
for (int i = 0; i < 10000; i++) { var num1 = int.Parse(word.AsSpan(0, splitIndex)); var num2 = int.Parse(word.AsSpan(splitIndex)); var sum = num1 + num2; }
而后在 托管堆 验证一下,是否是没有 临时 string 了?
0:000> !dumpheap -type String -stat Statistics: MT Count TotalSize Class Name 00007ffc53a51e18 167 36538 System.String
能够看到就只有 167 个系统字符串,性能也获得了不小的提高,🐂👃🦆。
平时用 Span 的时候,更多的会应用到 Array 上面,毕竟 Array 在托管堆上是连续内存,方便 Span 在上面画一个可视窗口,其实不只仅是 Array,从 .NET5 开始在 List 上画一个视图也是能够的,截图以下:
由于 List 的 CURD 会致使底层的 Array 忽长忽短或从新分配,也就没法实现物理上的连续内存,因此 Span 应用到 List 以后,但愿List是不可变的,这也是官方的建议。
总的来讲,Span 在 .NET 底层框架中的地位是愈来愈显著了,相信 netCore 追求更高更快的性能上 Span 必定大有可为,你们赶忙学起来,😀😀😀
更多高质量干货:参见个人 GitHub: dotnetfly