? ? 先談一下我對Span的看法, Span是指向任意連續內存空間的類型安全、內存安全的視圖,可操作的滑動窗口。
Span和Memory都是包裝了可以在pipeline上使用的結構化數據的內存緩沖器,他們被設計用于在pipeline中高效傳遞數據。
定語解讀
這里面許多定語,值得我們細細揣摩:
1. 指向任意連續內存空間:支持托管堆,原生內存、堆棧, 這個可從Span
的幾個重載構造函數窺視一二。2. 類型安全:Span?是一個泛型。
3. 內存安全:?Span[1]是一個
readonly ref struct
數據結構,用于表征一段連續內存的關鍵屬性被設置成只讀readonly, 保證了所有的操作只能在這段內存內。
//?截取自Span源碼?
public?readonly?ref?struct?Span<T>
{//?表征一段連續內存的關鍵屬性?Pointer?&?Length?都只能從構造函數賦值///?<summary>A?byref?or?a?native?ptr.</summary>internal?readonly?ByReference<T>?_reference;///?<summary>The?number?of?elements?this?Span?contains.</summary>private?readonly?int?_length;[MethodImpl(MethodImplOptions.AggressiveInlining)]public?Span(T[]??array){if?(array?==?null){this?=?default;return;?//?returns?default}if?(!typeof(T).IsValueType?&&?array.GetType()?!=?typeof(T[]))ThrowHelper.ThrowArrayTypeMismatchException();_reference?=?new?ByReference<T>(ref?MemoryMarshal.GetArrayDataReference(array));_length?=?array.Length;}
}
4. 視圖:操作結果會直接體現到底層的連續內存。
至此我們來看一個簡單的用法, 利用span操作指向一段堆棧空間。
static??void??Main(){Span<byte>?arraySpan?=?stackalloc?byte[100];??//?包含指針和Length的只讀指針,?類似于go里面的切片byte?data?=?0;for?(int?ctr?=?0;?ctr?<?arraySpan.Length;?ctr++)arraySpan[ctr]?=?data++;arraySpan.Fill(1);var?arraySum?=?Sum(arraySpan);Console.WriteLine($"The?sum?is?{arraySum}");???//?輸出100arraySpan.Clear();var?slice??=??arraySpan.Slice(0,50);?//?因為是只讀屬性,?內部New?Span<>(),?產生新的切片arraySum?=?Sum(slice);Console.WriteLine($"The?sum?is?{arraySum}");??//?輸出0}[MethodImpl(MethodImplOptions.AggressiveInlining)]static?int??Sum(Span<byte>?array){int?arraySum?=?0;foreach?(var?value?in?array)arraySum?+=?value;return?arraySum;}
??此處Span??指向了特定的堆棧空間, Fill,Clear 等操作的效果直接體現到該段內存。
??注意Slice切片方法,內部實質是產生新的Span,是一個新的視圖,對新span的操作會體現到原始底層數據結構。
[MethodImpl(MethodImplOptions.AggressiveInlining)]public?Span<T>?Slice(int?start){if?((uint)start?>?(uint)_length)ThrowHelper.ThrowArgumentOutOfRangeException();return?new?Span<T>(ref?Unsafe.Add(ref?_reference.Value,?(nint)(uint)start?/*?force?zero-extension?*/),?_length?-?start);}
? ? ? 從Slice切片源碼可以看到,實質是利用原ptr & length 產生包含新的ptr & length的操作視圖, ptr其實是指針的移動,也就是定位新的數據塊, 但是終歸是在原始數據塊內部。?
衍生技能點
我們再細看Span的定義, 有幾個關鍵詞建議大家溫故而知新。
1.?readonly strcut[2]
從C#7.2開始,你可以將readonly作用在struct上,指示該struct不可改變。
span?被定義為readonly struct,內部屬性自然也是readonly,從上面的分析和實例看我們可以針對Span表征的特定連續內存空間做內容更新操作;
如果想限制更新該連續內存空間的內容, C#提供了ReadOnlySpan<T>
類型, 該類型強調該塊內存只讀,也就是不存在Span?擁有的Fill,Clear等方法。
一線碼農大佬寫了文章講述[使用span對字符串求和]的姿勢,大家都說使用span能高效操作內存,我們對該用例BenchmarkDotNet壓測。
using?System;
using?System.Collections.Generic;
using?System.Linq;
using?System.Text;
using?System.Threading.Tasks;
using?System.Buffers;
using?System.Runtime.CompilerServices;
using?BenchmarkDotNet.Attributes;
using?BenchmarkDotNet.Running;namespace?ConsoleApp3
{public?class?Program{static??void?Main(){var?summary?=?BenchmarkRunner.Run<MemoryBenchmarkerDemo>();}}[MemoryDiagnoser,RankColumn]public?class?MemoryBenchmarkerDemo{int?NumberOfItems?=?100000;//?對字符串切割,?會產生字符串小對象[Benchmark]public?void??StringSplit(){for?(int?i?=?0;?i?<?NumberOfItems;?i++){var?s?=?"97?3";var?arr?=?s.Split(new?string[]?{?"?"?},?StringSplitOptions.RemoveEmptyEntries);var?num1?=?int.Parse(arr[0]);var?num2?=?int.Parse(arr[1]);_?=?num1?+?num2;}}//?對底層字符串切片[Benchmark]public?void?StringSlice(){for?(int?i?=?0;?i?<?NumberOfItems;?i++){var?s?=?"97?3";var?position?=?s.IndexOf('?');ReadOnlySpan<char>?span =?s.AsSpan();var?num1?=?int.Parse(span.Slice(0,?position));var?num2?=?int.Parse(span.Slice(position));_=?num1+?num2;}}}
}
壓測解讀:
?對字符串運行時切分,不會利用駐留池,于是case1會分配大量小對象;對gc造成壓力。??
?case2對底層字符串切片,雖然會產生不同的透視對象Span, 但是實際引用了的原始內存塊的偏移區間, 不存在分配新內存。
2.?ref struct[3]
從C#7.2開始,ref可以作用在struct,指示該類型被分配在堆棧上,并且不能轉義到托管堆。
Span,ReadonlySpan?包裝了對于任意連續內存快的透視操作,但是只能被存儲堆棧上,不適用于一些場景,例如異步調用,.NET Core 2.1為此新增了Memory[4]?, ReadOnlyMemory, 可以被存儲在托管堆上,這個暫時按下不表。
最后用一張圖總結, 本文成文,感謝[?yi念之間?]大佬參與討論。

引用鏈接
[1]
?Span:?https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Span.cs[2]
?readonly strcut:?https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#readonly-struct[3]
?ref struct:?https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct[4]
?Memory:?https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
與本文相關的經典文章
C#語法糖系列 —— 第四篇:聊聊 Span 的底層玩法
非常簡單的string駐留池,你對它真的了解嗎
年終總結:2021技術文大盤點 ?| ?打包過去,面向未來
項目總結:麻雀雖小,五臟俱全
理念總結:實話實說:只會.NET,會讓我們一直處于鄙視鏈、食物鏈的下游
云原生系列:?什么是云原生?
點“贊”戳“在看”
體現態度很有必要!