終于,他還是對C# Span 下手了,源碼解讀和應(yīng)用實踐

一:背景
1. 講故事
這兩天工作上太忙沒有及時持續(xù)的文章產(chǎn)出,和大家說聲抱歉,前幾天群里一個朋友在問什么時候可以產(chǎn)出 Span 的下一篇,哈哈,這就來啦!讀過上一篇的朋友應(yīng)該都知道 Span 統(tǒng)一了 .NET 程序?棧 + 托管 + 非托管?實現(xiàn)了三大塊內(nèi)存的統(tǒng)一訪問,??,而且在 .net 底層 Library 中也是一等公民的存在,很多現(xiàn)有的類都提供了對 Span / ReadOnlySpan 的支持。
String 對 Span / ReadOnlySpan 的支持
????public?sealed?class?String
????{
????????[MethodImpl(MethodImplOptions.InternalCall)]
????????[NullableContext(0)]
????????public?extern?String(ReadOnlySpan<char>?value);
????}
StringBuilder 對 Span / ReadOnlySpan 的支持
????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;
????????}
????}
Int 對 Span / ReadOnlySpan 的支持
????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));
????????}
????}
怎么樣,這些通用 & 基礎(chǔ)的類都在大力對接?Span / ReadOnlySpan,更別說復(fù)雜類型了,其地位不言自明哈,接下來我們就從 Span 本身的機制聊起。
二:Span 原理探究
1. Span 源碼分析
靈活運用 Span 解決工作中的實際問題我相信大家應(yīng)該沒什么毛病了,有了這個基礎(chǔ)再從 Span 的源碼 和 用戶態(tài) 和大家一起深度剖析,從源碼開始吧。
????public?readonly?ref?struct?Span
????{
????????internal?readonly?ByReference?_pointer;
????????private?readonly?int?_length;
????}
上面代碼的?ref struct?可以看出,這個 Span 是只可以分配在棧上的值類型,然后就是里面的 _pointer 和 _length 兩個實例字段,不知道看完這兩個字段腦子里是不是有一幅圖,大概是這樣的。

可以清晰的看出,Span 就是用來映射一段可以連續(xù)訪問的內(nèi)存地址,空間大小由 length 控制,開始位置由 _pointer 指定,是不是像極了指針???,是的,語言團隊要保證你的程序高性能,還得照護你的人身安全,出了各種手段,真是煞費苦心!???
2. Span 用戶態(tài)分析
雖然圖已經(jīng)畫了,但還是有很多朋友希望眼見為實,必須實操演練,嘿嘿,無懼任何挑戰(zhàn),那我先把上面的圖化成代碼:
????????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,棧內(nèi)容是:0x000001e956b8ab20,按照圖的理論:0x000001e956b8ab20 應(yīng)該是 nums 數(shù)組元素 1 的內(nèi)存地址,可以用 dp 驗證一下。
0:000>?dp?0x000001e956b8ab20
000001e9`56b8ab20??00000002`00000001?00000004`00000003
000001e9`56b8ab30??00000006`00000005?00000000`00000000
000001e9`56b8ab40??00007ffc`3e6c4388?00000000`00000000
從上面三行內(nèi)存地址來看,數(shù)組的:1,2,3,4,5,6?依次排列,有些朋友可能有點小疑問,為啥 nums 的內(nèi)存地址不是指向數(shù)組元素 1 的呢?那我來普及一下吧,先用 dp 喚出數(shù)組的內(nèi)存地址。
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 表示 數(shù)組 的 方法表地址,后面的 8byte 表示 6 ,也就是說數(shù)組有 6個元素,不信的話我截一張圖:

span 是由 _pointer + length 組成的,剛才的 _pointer 也給大家演示了,那 length 的值在哪里呢?因為 span 是 struct,所以需要用 dp 把剛才的線程棧最小的棧地址打出來就可以了。

到這里,我覺得我講的已經(jīng)夠清楚了,如果還有點懵的話可以仔細想一想哈。
三:Span 在 String 和 List 的實踐
Span的應(yīng)用場景真的是太多了,不可能在這篇一一列舉,這里我就舉兩個例子吧,讓大家能夠感受到 Span 的強大即可。
1. 在 String 上的應(yīng)用
案例:如何高效的計算出用戶輸入的值?10+20??
1) ?傳統(tǒng) Substring 做法
傳統(tǒng)的做法很簡單,截取唄,代碼如下:
????????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();
????????}

結(jié)果是很輕松的算出來了,但你仔細想想這里是不是有點什么問題,比如說為了從 word 中扣出 num,我用了兩次 SubString,就意味著會在 托管堆 上生成兩個 string,如果說我執(zhí)行 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 的個數(shù)
0:000>?!dumpheap?-type?String?-stat
Statistics:
??????????????MT????Count????TotalSize?Class?Name
00007ffc53a81e18????20167???????556538?System.String
托管堆上有 20167 個,挺恐怖的,真的是給 GC 添麻煩哈,這里還有 167 個是系統(tǒng)自帶的,接下來的問題是有沒有辦法替換 SubString 從而不生成臨時string呢?
2) ?新式 Span 做法
如果看懂了 Span 結(jié)構(gòu)圖,你就應(yīng)該會使用 _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 個系統(tǒng)字符串,性能也得到了不小的提升,???。
2. 在 List 上的應(yīng)用
平時用 Span 的時候,更多的會應(yīng)用到 Array 上面,畢竟 Array 在托管堆上是連續(xù)內(nèi)存,方便 Span 在上面畫一個可視窗口,其實不僅僅是 Array,從 .NET5 ?開始在 List 上畫一個視圖也是可以的,截圖如下:

因為 List 的 CURD 會導(dǎo)致底層的 Array 忽長忽短或重新分配,也就無法實現(xiàn)物理上的連續(xù)內(nèi)存,所以 Span 應(yīng)用到 List 之后,希望List是不可變的,這也是官方的建議。
四:總結(jié)
總的來說,Span 在 .NET 底層框架中的地位是越來越顯著了,相信 netCore 追求更高更快的性能上 Span 一定大有可為,大家趕緊學(xué)起來,???
【推薦】.NET Core開發(fā)實戰(zhàn)視頻課程?★★★
.NET Core實戰(zhàn)項目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實戰(zhàn)-統(tǒng)一身份認證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個接口多種實現(xiàn)的依賴注入與動態(tài)選擇看這篇就夠了
用abp vNext快速開發(fā)Quartz.NET定時任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實現(xiàn)作業(yè)調(diào)度
現(xiàn)身說法:實際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
