讀完上篇《通俗易懂,C#如何安全、高效地玩轉任何種類的記憶體之Span的本質(一)。》,相信大家對span的本質應該非常清楚了。含着金鑰匙出生的它,從小就被寄予厚望要成為.NET下編寫高性能應用程式的重要積木,而且很多老前輩為了接納它,都紛紛做出了改變,比如String、Int、Array。現在,它長大了,已經成為.NET下發揮關鍵作用的新值類型和旗艦成員。
那我們又該如何接納它呢?
一句話,熟悉它的脾氣秉性,讓好鋼用到刀刃上。
上篇部落格介紹了span的本質,主要涉及到三個字段,如下:
當我們通路span表示的整體或部分記憶體時,内部的索引器通過計算<code>(ref reference + byteOffset) + index * sizeOf(T)</code>來正确直接地傳回實際儲存位置的引用,而不是通過複制記憶體來傳回相對位置的副本,進而達到高性能,但是,現在我要告訴你,這種span被叫做slow span,為什麼呢?因為C#7.2的新特性<code>ref T</code>支援在簽名中直接傳回引用(相當于直接整合了這個過程),這樣就無需通過計算來确定指針開頭及其起始偏移,進而真正擁有和通路數組一樣高的效率,如下:
這種隻包含兩個字段的span就叫Fast span。
在所有的.NET平台,Slow Span都是可得到的,但是目前隻有.NET Core 2.X原生支援Fast span。
為了讓大家更直覺地了解這兩種Span,下面來做兩組基準測試
不同運作時下Span進行10萬次Get、Set的基準測試

上圖非常清楚了吧,從Mean(均值)名額可以看出差異還是比較大的(約60%),net framework時代追求生産力,而core時代追求高性能,是以還是早轉core吧,并且新版本core還會進一步優化span,差距将會越來越大。
Span vs Array的基準測試
不同運作時下,對Span和Array進行10萬次Get、Set操作
從上圖Mean(均值)名額可以得出:
slow span,即運作時原生不支援,在性能上,它的Get、Set操作和數組差異50%左右。
fast span,即運作時原生支援,在性能上,它的Get、Set操作和數組相當。
看了上面測試,可能有的同學就會問了用Array就行了,如果總是操作整個數組,這是合适的,但如果想操作數組的一部分資料呢?按照以前的做法每次複制一份相對位置的副本給調用方,這就非常消耗性能的,那麼如何支援對完整或部分數組的操作保持同樣高的性能呢?答案就是span,沒有之一。span不僅能用于通路數組和分離數組子集,還可引用來自記憶體任意區域的資料,比如本機代碼、棧記憶體、托管記憶體。
基準測試示例源碼參考
配置設定一塊棧記憶體是非常快速的,也無需手工釋放,它會随着目前作用域而釋放,比如方法執行結束時,就自動釋放了,是以需要快取快用快放。Span雖然支援所有類型的記憶體,但決定安全、高效地操作各種記憶體的下限自然取決于最嚴苛的記憶體類型,即棧記憶體,好比木桶能裝多少水,取決于最短的那塊木闆。此外,上一篇部落格的動畫非常清晰地示範了span的本質,每次都是通過整合内部指針為新的引用傳回,而.NET運作時跟蹤這些内部指針的成本非常高昂,是以将span限制為僅存在于棧上,進而隐式地限制了可以存在的内部指針數量。
備注:棧記憶體的容量非常小, ARM、x86 和 x64 計算機,預設堆棧大小為 1 MB。CLR和編譯器會自動檢測Stack-Only限制。
是以span必須是值類型,它不能被儲存到堆上。
Span不能作為類的字段。
Span不能實作任何接口
先來看一段C#(僞代碼):
使用ILDasm檢視生成的IL代碼:
上面的代碼很明确,首先讓自定義的值類型實作接口IEnumerable,然後作為參數傳遞給Parse,最後分析IL代碼發現參數被裝箱了,意味着将被儲存到托管堆上,如果将來C#能專門定義隻用于struct的接口,那麼就能擴充Stack-Only結構到此應用場景了,一起期待吧。
Span不能作為異步方法的參數
首先<code>async</code>和<code>await</code> 是非常棒的文法糖,不僅僅大大地簡化了編寫異步代碼的難度,而且還帶來了代碼的優雅度。
同樣,先來看一段C#代碼:
這樣的用法也是禁止的,編譯時就會報錯<code>Parameter or local type Span<byte> cannot be declared in async method.</code>。因為本質上,<code>async</code> & <code>await</code> 的内部是通過<code>AsyncMethodBuilder</code>來建立一個異步的狀态機,某一時刻可能會将方法參數儲存到托管堆上。
Span不能作為泛型類型的參數
這樣的用法也是禁止的,編譯時會報錯<code>The type Span<byte>may not be used as a type argument.</code>。同理,<code>span<byte></code>可以表示記憶體任意區域,而實際使用時肯定需要類型化對象,無法避免裝箱。那麼微軟為什麼不引入一種新的泛型限制:<code>stackonly</code>,而是決定禁止span作為泛型參數,因為這需要編譯器檢查所有的代碼,可能還需要了解代碼邏輯(因為有的類型需要運作時才能确定),不然是無法保證<code>stackonly</code>限制的,呵呵,目前看來是不現實的,不知人工智能能否解決這個問題。
闡述這個特點前,先簡單說說計算機的字大小。
計算機的字大小
表示計算機中CPU的字長,32位CPU字長為32位,即4位元組;64位CPU字長為64位,即8位元組。CPU的字長決定了每次能夠原子更新的連續記憶體塊的大小。
棧撕裂其實是多線程下的資料同步問題,當結構資料大于目前處理器的字大小時,都會面臨這個問題。如前所述,span内部包含多個字段,這就意味着,一些處理器可能無法保證原子更新<code>span</code>的<code>_reference</code>和<code>_length</code> 字段,也就是說,多線程下<code>_reference</code>和<code>_length</code>可能來自于兩個不同的span。
其實有兩種辦法可以解決這個問題:
直接處理 - 加鎖,即強制同步通路。
間接處理 - 私有化字段,即不給外面觀察到部分更新的機會。
如果這樣,就無法保證像數組一樣的高性能,是以不能給字段加鎖,也不能限制通路(沒意義),另外對<code>Span</code>的通路和寫入都是直接操作的記憶體,如果<code>_reference</code>和<code>_length</code>出現不同步的情況,還會導緻記憶體安全問題。
這也是為什麼span隻能存在于棧上,即指針、資料、長度全都存于棧上,而不是引用存在棧,資料存在堆,因為<code>span<T></code>不需要暫留,必須快取快用快放,否則就不要使用span。
備注:對于需要暫留到堆上的場景,它的解決方案是<code>Memory<T></code>,大家可以繼續關注。
為了支援輕松高效地處理 {ReadOnly}Span ,微軟向.NET添加了數百個新成員和類型。目前大多是基于數組、字元串和基元類型的方法的重載 ,除此之外,還包括一些專注于特定處理方面的全新類型,比如:System.IO.Pipelines。
下面是一些比較常用的擴充:
基元類型(僞代碼)
字元串
數組
Guid
最後使用上面的API示範一個官網的例子,解析字元串"123,456"中的數字:
以前的寫法:
現在的寫法:
當然還是有許多這樣的方法,比如System.Random、System.Net.Socket、Utf8Formatter、Utf8Parser等,明白了它的脾氣秉性,對于具體的應用場景大家可以先自行查閱資料,相信認真讀完上篇、本篇的同學已經具備用好這把尖刀的能力了。
綜上所訴,通過限制Span隻能駐留到棧上,完美解決了以下的問題:
更高效地記憶體通路,快取快用快放的天然保障。
更高效地GC跟蹤。
并發記憶體安全。
備注:正是由于Stack-Only這個特點,在底層資料通路、轉換以及同步處理方面,Span性能非常出色。
此外,本篇還在上篇的基礎上,詳細講解span的脾氣秉性,以及每種特點下的非法應用場景,一切都是為了大家能夠在.NET 程式中使用span高效安全地通路記憶體,希望大家能有所收獲。下一篇可能會講span的加強,也可能會講它在資料轉換以及同步處理方面的應用,比如:Data Pipelines、Discontinuous Buffers、Buffer Pooling等,也可能會講<code>Memory<T></code>,感興趣請繼續關注。
如果有什麼疑問和見解,歡迎評論區交流。
如果你覺得本篇文章對您有幫助的話,感謝您的【推薦】。
如果你對高性能程式設計感興趣的話可以關注我,我會定期的在部落格分享我的學習心得。
歡迎轉載,請在明顯位置給出出處及連結。
https://adamsitnik.com/Hardware-Counters-Diagnoser/#how-to-get-it-running-for-net-coremono-on-windows
https://blogs.msdn.microsoft.com/dotnet/2017/10/16/ryujit-just-in-time-compiler-optimization-enhancements
https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.Fast.cs
https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.cs
https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code
做一個有底蘊的軟體工作者