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

系列介紹

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

正文

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

它與我們傳統使用的基礎類型相比具備超高的性能,緣由是它減小了大量的內存分配和數據量複製,而且它所分配的數據內存是連續的。github

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

x

特別是在AspNetCore中,我們會使用到大量的異步操做方法。「因此,這個時候若是咱們又想跨線程操做數據又想得到相似Span這樣的性能怎麼辦呢?」 上一篇文章咱們留下了這樣的一個問題,因此如今就是到了還願的時候了。它就是與Span一塊兒發佈的孿生兄弟: Memory性能優化

x

獅子座和射手座黃金聖鬥士一樣具有超越光速的能力框架

什麼是Memory

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

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 關鍵字了。異步

因此,咱們看到它的內部結構就是醬紫的:async

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屬性等。ide

被聲明爲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

相關文章
相關標籤/搜索