【5min+】傳說中的孿生兄弟? Memory and Span


系列介紹

【五分鍾的dotnet】是一個利用您的碎片化時間來學習和豐富.net知識的博文系列。它所包含了.net體系中可能會涉及到的方方面面,比如C#的小細節,AspnetCore,微服務中的.net知識等等。
5min+不是超過5分鍾的意思,"+"是知識的增加。so,它是讓您花費5分鍾以下的時間來提升您的知識儲備量。

正文

在上一篇文章:《閃電光速拳? .NetCore 中的Span》 中我們提到了在.net core 2.x 所新增的一個類型:Span

它與咱們傳統使用的基礎類型相比具有超高的性能,原因是它減少了大量的內存分配和數據量復制,並且它所分配的數據內存是連續的。

但是您會發現它無法用在我們項目的某些地方,它獨特的 ref結構 使它沒有辦法跨線程使用、更沒有辦法使用Lambda表達式。

x

特別是在AspNetCore中,咱們會使用到大量的異步操作方法。“所以,這個時候如果我們又想跨線程操作數據又想獲得類似Span這樣的性能怎么辦呢?” 上一篇文章我們留下了這樣的一個問題,所以現在就是到了還願的時候了。它就是與Span一起發布的孿生兄弟: Memory

x

獅子座和射手座黃金聖斗士同樣具備超越光速的能力

什么是Memory

那什么是Memory呢?不妨我們先來猜測一下,它的結構是什么樣子。畢竟它是Span的孿生兄弟,而Span的結構我們在前面就了解過了:

public readonly ref struct Span<T>
{
    public void Clear();
    public void CopyTo([NullableAttribute(new[] { 0, 1 })] Span<T> destination);
    public void Fill(T value);
    public Enumerator GetEnumerator();
    public Span<T> Slice(int start, int length);
    public T[] ToArray();
    public override string ToString();

    //.....
}

當時我們說Span有各種缺陷的原因是由於它獨特的 ref struct 關鍵字所導致的,導致它無法拆箱裝箱、無法書寫Lambda、無法跨線程等。但是它兄弟卻可以克服缺點,所以我們想想它會和Span在聲明上有哪些差距呢? 是的,您可能已經想到了:它不會有 ref 關鍵字了。

所以,我們看到它的內部結構就是醬紫的:

public readonly struct Memory<T>
{
    public static Memory<T> Empty { get; }
    public bool IsEmpty { get; }
    public int Length { get; }
    public Span<T> Span { get; }
    public void CopyTo([NullableAttribute(new[] { 0, 1 })] Memory<T> destination);
    public MemoryHandle Pin();
    public Memory<T> Slice(int start, int length);
    public T[] ToArray();
    public override string ToString();
}

和我們猜想的一樣。它少了ref關鍵字,內部方法也和Span差不多(同樣擁有CopyTo,Slice等),但是還是有一些差異,比如多了Pin方法,Span屬性等。

被聲明為ref struct的結構,叫做“ByRefLike”。所以在我們在進行反射的時候,我們使用Type會看到有這樣一個屬性:IsByRefLike

x

好像有點超綱了哈(>人<;)

按照MSDN給出的解釋:

該結構是使用中的C# ref struct 關鍵字聲明的。 不能將類似 byref 的結構的實例放置在托管堆上。

所以這也是為什么上一篇文章說的:Span只能放置在內存棧中的原因。

那么反過來想,沒有了ref關鍵字之后。Memory是不是就可以放置在托管堆上了呢?是不是就可以進行拆裝箱,克隆副本供其它線程的內存棧使用了呢? 好吧,可能是這樣。所以這也許就是它能夠被允許跨線程使用的原因吧。

進行到了這一步,那我們再回過頭來想想Memory是什么呢? 其實現在我們心里其實都已經有個底了:

與 Span<T>一樣,Memory<T> 表示內存的連續區域。 但 Span<T>不同,Memory<T> 不是ref 結構。 這意味着 Memory<T> 可以放置在托管堆上,而 Span<T> 不能。 因此,Memory<T> 結構與 Span<T> 實例沒有相同的限制。 具體而言:

  • 它可用作類中的字段。
  • 它可跨 await 和 yield 邊界使用。

除了 Memory<T>之外,還可以使用 System.ReadOnlyMemory<T> 來表示不可變或只讀內存。

這是MSDN給出來的解釋,不是我亂編的哈😝!(雖然和我們上面猜的一模一樣(●ˇ∀ˇ●)

接下來,我們來看看他們到底有多像:

x

好吧,為了做該圖我已經使用了美工必殺器 - ps😭

有沒有發現,除了名字之外,好像其它的都一模一樣😱。甚至直接連注釋都懶得改了。

一樣卻又不一樣

既然作為孿生兄弟,必然有一些共通之處。而Memory作為對Span的增強(應該也算不算增強吧),那么內部的實現可能很多會與Span相似。

是的,查看Memory的源代碼您就會發現,它的內部某些方法就是通過Span來實現的:

public readonly struct Memory<T>
{
    public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span);
    public T[] ToArray() => Span.ToArray();
}

有關Memory的源代碼,您可以點此查看:the source code of Memory

所以您會發現Memory是可以直接轉換為Span的。但是Memory作為一個可以跨線程的類型被轉換為Span是相對危險的,所以Dotnet Core的開發人員直接在備注上寫了這樣的文字:

Such a cast can only be done with unsafe or marshaling code,in which case that's the dangerous operation performed by the dev, and we're just following suit here to make it work as best as possible.

意思就是這種轉換很危險,我來幫你做了算了。

x

如何使用

來吧,修改上面的Span會在Task中報錯的例子:

public async Task MemoryCanInLambda(Memory<string> buffer)
{
    await Task.Factory.StartNew(() =>
    {
        buffer.Trim("s");
    });
}

此時我們就可以在異步中使用Memory了,采用連續內存+指針級別的操作方案來操作數據內容,豈不爽歪歪?

異步的數據交由Memory,同步的數據交由Span,ForExample:


static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream)
{
  int bytesRead = await stream.ReadAsync(buffer);
  return Checksum(buffer.Span.Slice(0, bytesRead));
  // Or buffer.Slice(0, bytesRead).Span
}
static int Checksum(Span<byte> buffer) { ... }

正是由於SpanMemory帶來的巨大性能優化,所以.NET Core的開發者們做了一件非常瘋狂的事:為.NET的庫添加了數百個重載方法。 比如,您現在可以看到我們經常使用的Int.Parse方法居然支持了Span,它的簽名是醬紫:

public static Int32 Parse(ReadOnlySpan<char> s, NumberStyles style = NumberStyles.Integer, [NullableAttribute(2)] IFormatProvider? provider = null);

除此之外,還有longdouble…………甚至連Guid和DateTime都有這樣的重載。
還有其它常用的各種類也開始支持以Span作為參數的重載方法了,比如Random、StringBuilder等。

public StringBuilder Append(ReadOnlySpan<char> value);

先不談重建這些基礎常用類型的重載工作量有多大,我們應該想想.NET為什么要這么做呢?就是為了我們能夠使用SpanMemory來代替我們現有的一些操作,從而提升性能。

那么僅僅是開發底層框架才適合用它們嗎? 當然不是,就好比是截取字符串的操作,無論是底層框架還是應用程序級別的代碼都會用到。所以如果有可能,而當我們的項目又正好是.netCore 2.x以上的版本,為何不去嘗試使用下呢?

不要因為“我知道Span不過就是把原有的某某操作放到內存某處,不過如此”,就對它產生偏見。確實,Span的實現很簡單,您如果有興趣可以查看它的實現代碼。.net core正在為它的實現和使用做巨大的適配工作,C# 從7.x 開始就不斷對異步操作和內存分配進行優化,這或許也為我們未來.NET的發展給了一點點提示。加油,偉大的開發人員們。(ง •_•)ง

最后,小聲說一句:創作不易,點個推薦吧😇

x


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM