一:背景
1. 講故事
這兩天作業上太忙沒有及時持續的文章產出,和大家說聲抱歉,前幾天群里一個朋友在問什么時候可以產出 Span 的下一篇,哈哈,這就來啦!讀過上一篇的朋友應該都知道 Span 統一了 .NET 程式 堆疊 + 托管 + 非托管 實作了三大塊記憶體的統一訪問,????,而且在 .net 底層 Library 中也是一等公民的存在,很多現有的類都提供了對 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));
}
}
怎么樣,這些通用 & 基礎的類都在大力對接 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 = 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 喚出陣列的記憶體地址,
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 一定大有可為,大家趕緊學起來,??????
更多高質量干貨:參見我的 GitHub: dotnetfly
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/215862.html
標籤:.NET技术
