這兩天工做上太忙沒有及時持續的文章產出,和你們說聲抱歉,前幾天羣裏一個朋友在問何時能夠產出 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