C# Span 源码解读和应用实践
一:背景
1. 講故事
這兩天工作上太忙沒有及時持續的文章產出,和大家說聲抱歉,前幾天群里一個朋友在問什么時候可以產出 Span 的下一篇,哈哈,這就來啦!讀過上一篇的朋友應該都知道 Span 統一了 .NET 程序 棧 + 托管 + 非托管 實現了三大塊內存的統一訪問,????????,而且在 .net 底層 Library 中也是一等公民的存在,很多現有的類都提供了對 Span / ReadOnlySpan 的支持。
String 對 Span / ReadOnlySpan 的支持
StringBuilder 對 Span / ReadOnlySpan 的支持
Int 對 Span / ReadOnlySpan 的支持
怎么樣,這些通用 & 基礎的類都在大力對接 Span / ReadOnlySpan,更別說復雜類型了,其地位不言自明哈,接下來我們就從 Span 本身的機制聊起。
二:Span 原理探究
1. Span 源碼分析
靈活運用 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 指定,是不是像極了指針????????????,是的,語言團隊要保證你的程序高性能,還得照護你的人身安全,出了各種手段,真是煞費苦心!????????????
2. Span 用戶態分析
雖然圖已經畫了,但還是有很多朋友希望眼見為實,必須實操演練,嘿嘿,無懼任何挑戰,那我先把上面的圖化成代碼:
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?=?0x000001e956b8ab100x000000963277E608?=?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 喚出數組的內存地址。
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個元素,不信的話我截一張圖:
span 是由 _pointer + length 組成的,剛才的 _pointer 也給大家演示了,那 length 的值在哪里呢?因為 span 是 struct,所以需要用 dp 把剛才的線程棧最小的棧地址打出來就可以了。
到這里,我覺得我講的已經夠清楚了,如果還有點懵的話可以仔細想一想哈。
三:Span 在 String 和 List 的實踐
Span的應用場景真的是太多了,不可能在這篇一一列舉,這里我就舉兩個例子吧,讓大家能夠感受到 Span 的強大即可。
1. 在 String 上的應用
案例:如何高效的計算出用戶輸入的值 10+20 ?
1) ?傳統 Substring 做法
傳統的做法很簡單,截取唄,代碼如下:
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呢?
2) ?新式 Span 做法
如果看懂了 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 個系統字符串,性能也得到了不小的提升,????????????。
2. 在 List 上的應用
平時用 Span 的時候,更多的會應用到 Array 上面,畢竟 Array 在托管堆上是連續內存,方便 Span 在上面畫一個可視窗口,其實不僅僅是 Array,從 .NET5 ?開始在 List 上畫一個視圖也是可以的,截圖如下:
因為 List 的 CURD 會導致底層的 Array 忽長忽短或重新分配,也就無法實現物理上的連續內存,所以 Span 應用到 List 之后,希望List是不可變的,這也是官方的建議。
四:總結
總的來說,Span 在 .NET 底層框架中的地位是越來越顯著了,相信 netCore 追求更高更快的性能上 Span 一定大有可為,大家趕緊學起來,????????????
總結
以上是生活随笔為你收集整理的C# Span 源码解读和应用实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 起点低,怎么破?
- 下一篇: [C#.NET 拾遗补漏]12:死锁和活