1. 程式人生 > >《ASP.NET Core 高效能系列》Span<T>和Memory<T>

《ASP.NET Core 高效能系列》Span<T>和Memory<T>

一、Span<T>概述

  原文:Provides a type- and memory-safe representation of a contiguous region of arbitrary memory.

中文的翻譯不準確,這裡給出比較厚道的翻譯:提供型別T安全、連續的記憶體區域的表達方式.

    (圖1:Span<T>定義,不是全圖)

 

   這裡出現高階語法 readonly ref struct,下面是msdn給的語言規範(或者其核心意義),估計大家會看暈, 

     Span<T> 並且不能跨 await 和 yield 邊界使用。 此外,對兩個方法的呼叫(Equals(Object) 和 GetHashCode)將引發一個 NotSupportedException。因為鎖定在堆疊上,所以也不要試圖讓其成為做為靜態成員。

 

我先給出最簡單的解釋:

  Span<T>是微軟為了給.NET提供了一個高效的記憶體操縱元素,而定義的一個數據結構,為了高效的初衷,將Span<T>自身鎖定在堆疊上(記憶體連續,且處理高效)

注意:是Span<T>自身!!!Span<T> 例項通常用於儲存陣列或某個陣列的一部分的元素。

 二、Span<T>可用來做哪些事

  2.1 不得不提的 Slice

  切片這種東西,在GO,Rust中太尋常了(PS:當然對於C++的表示不屑),對於C#而言,這是一個性能提升不可或缺的概念,

Span基本上就是這個概念的翻版.所以其中有諸多方法就是切片.

  

 

 

 

 

 可見微軟為Span提供了諸多類似於原來的String中的很多方法,具體查閱地址: Span的擴充套件方法

 

 2.1 切片是其本質,是對原有物件的投影(或部分投影)

  之前我們要實現高效的操作,如字串類的操作,陣列類的操作,

  這裡應該尤其注意,不見得你使用Span就高效了,明白它的設計初衷:Slice! 特別是會不斷產生新的碎片和構造新物件的場景.(由此可見對於String的操作產生了諸多

新碎片這樣的場景是尤其好用的)

  我們看看如下的場景,大家覺得哪個效率會更高

            int[] array = new int[10000];
            Span<int> arraySpan = array;

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            for (int ctr = 0; ctr < arraySpan.Length; ctr++)
                arraySpan[ctr] = arraySpan[ctr] * arraySpan[ctr]/3;
            stopwatch.Stop();
            Console.WriteLine(stopwatch.Elapsed);

            array = new int[10000];
            stopwatch.Reset();
            stopwatch.Start();
            for (int ctr = 0; ctr < array.Length; ctr++)
                array[ctr] = array[ctr] * array[ctr]/3;
            Console.WriteLine(stopwatch.Elapsed);        

  結果按照我們的原則你就知道,不用Span效果會更好,下圖是realse模式下發布的,整體上可知:不用Span會更快,所以切片是它的本質!大家看看GO的切片就知道了.

  

 

 3.1 Span<T>可以不僅投影常見物件還可以是從Marshal , stackalloc分配的而來的

 

var native = Marshal.AllocHGlobal(100);
Span<byte> nativeSpan;
unsafe
{
    nativeSpan = new Span<byte>(native.ToPointer(), 100);
}
byte data = 0;
for (int ctr = 0; ctr < nativeSpan.Length; ctr++)
    nativeSpan[ctr] = data++;

int nativeSum = 0;
foreach (var value in nativeSpan)
    nativeSum += value;

Console.WriteLine($"The sum is {nativeSum}");
Marshal.FreeHGlobal(native);

 

byte data = 0;
Span<byte> stackSpan = stackalloc byte[100];
for (int ctr = 0; ctr < stackSpan.Length; ctr++)
    stackSpan[ctr] = data++;

int stackSum = 0;
foreach (var value in stackSpan)
    stackSum += value;

Console.WriteLine($"The sum is {stackSum}");

  

三、Memory<T>概述

  和Span<T>類似,它同樣表示連續記憶體區域。區別是沒有Span<T>堆疊上的限制,沒有 readonly ref struct 這樣的申明瞭.

這意味著 Memory<T> 可以放置在託管堆上,而 Span<T> 不能。 因此,Memory<T> 結構與 Span<T> 例項沒有相同的限制。

具體而言:它可用作類中的欄位。它可跨 await 和 yield 邊界使用。除了 Memory<T>之外,還可以使用 System.ReadOnlyMemory<T> 來表示不可變或只讀記憶體。

 

 

  這裡有園友從C++原始碼的角度進行分析,這裡提取下面兩段,供大家參閱(連結地址),

Span 與 Memory 的區別:

  1.Memory<T> 儲存 原有的物件地址、子內容的開始地址 與 子內容的長度,大致情況下圖:

  

 

  如上文所說,Span被微軟鎖定在堆疊上,

  2.Span 儲存子內容的開始地址與長度,不儲存原始物件的地址,大致如下圖:

  

 

 

如果這就是真實情況,可見Span脫離不了堆疊的環境的,不然計算不了真實的切片地址的.

 

三、使用上的預測和建議

  1.類似於string這樣的操作會Span大有用處(因為會產生很多新的中間資料產生開銷)

  2.無論span還是memory設計初衷就是Slice,使用場景是那些會不斷產生新的開銷、新的物件的場景.

  3.其實所有的效能提升根本讓CPU執行的指令更少了,減少了不必要的開銷