C# Span 源碼解讀和應用實踐

一:背景

1. 講故事

這兩天工做上太忙沒有及時持續的文章產出,和你們說聲抱歉,前幾天羣裏一個朋友在問何時能夠產出 Span 的下一篇,哈哈,這就來啦!讀過上一篇的朋友應該都知道 Span 統一了 .NET 程序 棧 + 託管 + 非託管 實現了三大塊內存的統一訪問,🐂👃,並且在 .net 底層 Library 中也是一等公民的存在,不少現有的類都提供了對 Span / ReadOnlySpan 的支持。git

  • 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 自己的機制聊起。github

二: 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 用戶態分析

雖然圖已經畫了,但仍是有不少朋友但願眼見爲實,必須實操演練,嘿嘿,無懼任何挑戰,那我先把上面的圖化成代碼: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 在 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

圖片名稱
相關文章
相關標籤/搜索