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

可以清晰的看出,Span 就是用來映射一段可以連續(xù)訪問的內(nèi)存地址,空間大小由 length 控制,開始位置由 _pointer 指定,是不是像極了指針???,是的,語(yǔ)言團(tuán)隊(duì)要保證你的程序高性能,還得照護(hù)你的人身安全,出了各種手段,真是煞費(fèi)苦心!???
2. Span 用戶態(tài)分析
雖然圖已經(jīng)畫了,但還是有很多朋友希望眼見為實(shí),必須實(shí)操演練,嘿嘿,無懼任何挑戰(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,按照?qǐng)D的理論:0x000001e956b8ab20 應(yīng)該是 nums 數(shù)組元素 1 的內(nèi)存地址,可以用 dp 驗(yàn)證一下。
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 依次排列,有些朋友可能有點(diǎn)小疑問,為啥 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個(gè)元素,不信的話我截一張圖:

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

到這里,我覺得我講的已經(jīng)夠清楚了,如果還有點(diǎn)懵的話可以仔細(xì)想一想哈。
三:Span 在 String 和 List 的實(shí)踐
Span的應(yīng)用場(chǎng)景真的是太多了,不可能在這篇一一列舉,這里我就舉兩個(gè)例子吧,讓大家能夠感受到 Span 的強(qiáng)大即可。
1. 在 String 上的應(yīng)用
案例:如何高效的計(jì)算出用戶輸入的值 10+20 ?
1) ?傳統(tǒng) Substring 做法
傳統(tǒng)的做法很簡(jiǎn)單,截取唄,代碼如下:
????????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é)果是很輕松的算出來了,但你仔細(xì)想想這里是不是有點(diǎn)什么問題,比如說為了從 word 中扣出 num,我用了兩次 SubString,就意味著會(huì)在 托管堆 上生成兩個(gè) string,如果說我執(zhí)行 1w 次話,那托管堆上會(huì)不會(huì)有 2w 個(gè) 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 的個(gè)數(shù)
0:000>?!dumpheap?-type?String?-stat
Statistics:
??????????????MT????Count????TotalSize?Class?Name
00007ffc53a81e18????20167???????556538?System.String
托管堆上有 20167 個(gè),挺恐怖的,真的是給 GC 添麻煩哈,這里還有 167 個(gè)是系統(tǒng)自帶的,接下來的問題是有沒有辦法替換 SubString 從而不生成臨時(shí)string呢?
2) ?新式 Span 做法
如果看懂了 Span 結(jié)構(gòu)圖,你就應(yīng)該會(huì)使用 _pointer + length 將 string 進(jìn)行切片處理,對(duì)不對(duì),代碼如下:
????????????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;?
????????????}
然后在 托管堆 驗(yàn)證一下,是不是沒有 臨時(shí) string 了?
0:000>?!dumpheap?-type?String?-stat
Statistics:
??????????????MT????Count????TotalSize?Class?Name
00007ffc53a51e18??????167????????36538?System.String
可以看到就只有 167 個(gè)系統(tǒng)字符串,性能也得到了不小的提升,???。
2. 在 List 上的應(yīng)用
平時(shí)用 Span 的時(shí)候,更多的會(huì)應(yīng)用到 Array 上面,畢竟 Array 在托管堆上是連續(xù)內(nèi)存,方便 Span 在上面畫一個(gè)可視窗口,其實(shí)不僅僅是 Array,從 .NET5 ?開始在 List 上畫一個(gè)視圖也是可以的,截圖如下:

因?yàn)?List 的 CURD 會(huì)導(dǎo)致底層的 Array 忽長(zhǎng)忽短或重新分配,也就無法實(shí)現(xiàn)物理上的連續(xù)內(nèi)存,所以 Span 應(yīng)用到 List 之后,希望List是不可變的,這也是官方的建議。
四:總結(jié)
總的來說,Span 在 .NET 底層框架中的地位是越來越顯著了,相信 netCore 追求更高更快的性能上 Span 一定大有可為,大家趕緊學(xué)起來,???


終于GitHub App 已支持簡(jiǎn)體中文!

微信錢包“免費(fèi)提現(xiàn)”的方法來了!
