<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          關(guān)于C# Span的一些實(shí)踐

          共 5121字,需瀏覽 11分鐘

           ·

          2020-12-24 08:44


          Span這個(gè)東西出來很久了,居然因?yàn)?.0又火起來了。

          特別感謝RC兄弟提出這個(gè)話題。

          ?

          相關(guān)知識(shí)

          在大多數(shù)情況下,C#開發(fā)時(shí),我們只使用托管內(nèi)存。而實(shí)際上,C#為我們提供了三種類型的內(nèi)存:

          • 堆棧內(nèi)存 - 最快速的內(nèi)存,能夠做到極快的分配和釋放。堆棧內(nèi)存使用時(shí),需要用stackalloc進(jìn)行分配。堆棧的一個(gè)特點(diǎn)是空間非常小(通常小于1 MB),適合CPU緩存。試圖分配更多堆棧會(huì)報(bào)出StackOverflowException錯(cuò)誤并終止進(jìn)程;另一個(gè)特點(diǎn)是生命周期非常短 - 方法結(jié)束時(shí),堆棧會(huì)與方法的內(nèi)存一起釋放。stackalloc通常用于必須不分配任何托管內(nèi)存的短操作。一個(gè)例子是在corefx中記錄快速記錄ETW事件:要求盡可能快,并且需要很少的內(nèi)存。

          • 非托管內(nèi)存 - 通過Marshal.AllocHGlobalxMarshal.AllocCoTaskMem方法分配在非托管堆上的內(nèi)存。這個(gè)內(nèi)存對(duì)GC不可見,并且必須通過Marshal.FreeHGlobalMarshal.FreeCoTaskMem的顯式調(diào)用來釋放。使用非托管內(nèi)存,最主要的目的是不給GC增加額外的壓力,所以最經(jīng)常的使用方式是在分配大量沒有指針的值類型時(shí)使用。在Kestrel的代碼中,很多地方用到了非托管內(nèi)存。

          • 托管內(nèi)存 - 大多數(shù)代碼中最常用的內(nèi)存,需要用new操作符來分配。之所以稱為托管(managed),因?yàn)樗潜籊C(垃圾管理器)管理的,由GC決定何時(shí)釋放內(nèi)存,而不需要開發(fā)人員考慮。GC又將托管對(duì)象根據(jù)大?。?5000字節(jié))分為大對(duì)象和小對(duì)象。兩個(gè)對(duì)象的分配方式、速度和位置都有不同,小對(duì)象相對(duì)快點(diǎn),大對(duì)象相對(duì)慢點(diǎn)。另外,兩種對(duì)象的GC回收成本也不一樣。


          問題的產(chǎn)生

          問個(gè)問題:寫了這么多年的C#,我們有用過指針嗎?有沒有想過為什么?

          我們用個(gè)例子來回答這個(gè)問題:一個(gè)字符串,正常它是一個(gè)托管對(duì)象。

          如果我們想解析整個(gè)字符串,我們會(huì)這么寫:

          int?Parse(string?managedMemory);

          那么,如果我們想只解析一部分字符串,該怎么寫?

          int?Parse(string?managedMemory,?int?startIndex,?int?length);

          現(xiàn)在,我們轉(zhuǎn)到非托管內(nèi)存上:

          unsafe?int?Parse(char*?pointerToUnmanagedMemory,?int?length);
          unsafe?int?Parse(char*?pointerToUnmanagedMemory,?int?startIndex,?int?length);

          再延伸一下,我們寫幾個(gè)用于復(fù)制內(nèi)存的功能:

          void?Copy(T[]?source,?T[]?destination);?
          void?Copy(T[]?source,?int?sourceStartIndex,?T[]?destination,?int?destinationStartIndex,?int?elementsCount);
          unsafe?void?Copy(void*?source,?void*?destination,?int?elementsCount);
          unsafe?void?Copy(void*?source,?int?sourceStartIndex,?void*?destination,?int?destinationStartIndex,?int?elementsCount);
          unsafe?void?Copy(void*?source,?int?sourceLength,?T[]?destination);
          unsafe?void?Copy(void*?source,?int?sourceStartIndex,?T[]?destination,?int?destinationStartIndex,?int?elementsCount);

          是不是很復(fù)雜?而且看上去并不安全?

          所以,問題并不在于我們能不能用,而在于這種支持會(huì)讓代碼變得復(fù)雜,而且并不安全 - 直到Span出現(xiàn)。

          Span

          在定義中,Span就是一個(gè)簡單的值類型。它真正的價(jià)值,在于允許我們與任何類型的連續(xù)內(nèi)存一起工作。

          這些所謂的連續(xù)內(nèi)存,包括:

          • 非托管內(nèi)存緩沖區(qū)

          • 數(shù)組和子串

          • 字符串和子字符串

          在使用中,Span確保了內(nèi)存和數(shù)據(jù)安全,而且?guī)缀鯖]有開銷。

          使用Span

          要使用Span,需要設(shè)置開發(fā)語言為C# 7.2以上,并引用System.Memory到項(xiàng)目。

          <PropertyGroup>
          ??<LangVersion>7.2LangVersion>
          PropertyGroup>

          使用低版本編譯器,會(huì)報(bào)錯(cuò):Error CS8107 Feature 'ref structs' is not available in C# 7.0. Please use language version 7.2 or greater.。

          ?

          Span使用時(shí),最簡單的,可以把它想象成一個(gè)數(shù)組,它會(huì)做所有的指針運(yùn)算,同時(shí),內(nèi)部又可以指向任何類型的內(nèi)存。

          例如,我們可以為非托管內(nèi)存創(chuàng)建Span:

          Span?stackMemory?=?stackalloc?byte[256];

          IntPtr?unmanagedHandle?=?Marshal.AllocHGlobal(256);
          Span?unmanaged?=?new?Span(unmanagedHandle.ToPointer(),?256);?
          Marshal.FreeHGlobal(unmanagedHandle);

          T[]到Span的隱式轉(zhuǎn)換:

          char[]?array?=?new?char[]?{?'i',?'m',?'p',?'l',?'i',?'c',?'i',?'t'?};
          Span<char>?fromArray?=?array;

          ?

          此外,還有ReadOnlySpan,可以用來處理字符串或其他不可變類型:

          ReadOnlySpan<char>?fromString?=?"Hello?world".AsSpan();

          ?

          Span創(chuàng)建完成后,就跟普通的數(shù)組一樣,有一個(gè)Length屬性和一個(gè)允許讀寫的index,因此使用時(shí)就和一般的數(shù)組一樣使用就好。

          看看Span常用的一些定義、屬性和方法:

          Span(T[]?array);
          Span(T[]?array,?int?startIndex);
          Span(T[]?array,?int?startIndex,?int?length);
          unsafe?Span(void*?memory,?int?length);

          int?Length?{?get;?}
          ref?T?this[int?index]?{?get;?set;?}

          Span?Slice(int?start);
          Span?Slice(int?start,?int?length);

          void?Clear();
          void?Fill(T?value);

          void?CopyTo(Span?destination);
          bool?TryCopyTo(Span?destination);

          ?

          我們用Span來實(shí)現(xiàn)一下文章開頭的復(fù)制內(nèi)存的功能:

          int?Parse(ReadOnlySpan<char>?anyMemory);
          int?Copy(ReadOnlySpan?source,?Span?destination);

          看看,是不是非常簡單?

          而且,使用Span時(shí),運(yùn)行性能極佳。關(guān)于Span的性能,網(wǎng)上有很多評(píng)測(cè),關(guān)注的兄弟可以自己去看。

          Span的限制

          Span支持所有類型的內(nèi)存,所以,它也會(huì)有相當(dāng)嚴(yán)格的限制。

          在上面的例子中,使用的是堆棧內(nèi)存。所有指向堆棧的指針都不能存儲(chǔ)在托管堆上。因?yàn)榉椒ńY(jié)束時(shí),堆棧會(huì)被釋放,指針會(huì)變成無效值,如果再使用,就是內(nèi)存溢出。

          因此:Span實(shí)例也不能駐留在托管堆上,而只能駐留在堆棧上。這又引出一些限制。

          1. Span不能是非堆棧類型的字段

          如果在類中設(shè)置Span字段,它將被存儲(chǔ)在堆中。這是不允許的:

          class?Impossible
          {

          ????Span?field;
          }

          不過,從C# 7.2開始,在其他僅限堆棧的類型中有Span字段是可以的:

          ref?struct?TwoSpans
          {

          ????public?Span?first;
          ????public?Span?second;
          }?
          1. Span不能有接口實(shí)現(xiàn)

          接口實(shí)現(xiàn)意味著數(shù)據(jù)會(huì)被裝箱。而裝箱意味著存儲(chǔ)在堆中。同時(shí),為了防止裝箱,Span必須不實(shí)現(xiàn)任何現(xiàn)有的接口,例如最容易想到的IEnumerable。也許某一天,C#會(huì)允許定義由結(jié)構(gòu)體實(shí)現(xiàn)的結(jié)口?

          1. Span不能是異步方法的參數(shù)

          異步在C#里絕對(duì)是個(gè)好東西。

          不過對(duì)于Span,是另一件事。異步方法會(huì)創(chuàng)建一個(gè)AsyncMethodBuilder構(gòu)建器,構(gòu)建器會(huì)創(chuàng)建一個(gè)異步狀態(tài)機(jī)。異步狀態(tài)機(jī)會(huì)將方法的參數(shù)放到堆上。所以,Span不能用作異步方法的參數(shù)。

          1. Span不能是泛型的代入?yún)?shù)

          看下面的代碼:

          Span?Allocate()?=>?new?Span(new?byte[256]);

          void?CallAndPrint(Func?valueProvider)?
          {
          ????object?value?=?valueProvider.Invoke();

          ????Console.WriteLine(value.ToString());
          }

          void?Demo()
          {
          ????Func>?spanProvider?=?Allocate;
          ????CallAndPrint>(spanProvider);
          }

          同樣也是裝箱的原因。

          ?

          上面是Span的內(nèi)容。

          下面簡單說一下另一個(gè)經(jīng)常跟Span一起提的內(nèi)容:Memory

          Memory

          Memory是一個(gè)新的數(shù)據(jù)類型,它只能指向托管內(nèi)存,所以不具有僅限堆棧的限制。

          Memory可以從托管數(shù)組、字符串或IOwnedMemory中創(chuàng)建,傳遞給異步方法或存儲(chǔ)在類的字段中。當(dāng)需要Span時(shí),就調(diào)用它的Span屬性。它會(huì)根據(jù)需要?jiǎng)?chuàng)建Span。然后在當(dāng)前范圍內(nèi)使用它。

          看一下Memory的主要定義、屬性和方法:

          public?readonly?struct?Memory
          {

          ????private?readonly?object?_object;
          ????private?readonly?int?_index;
          ????private?readonly?int?_length;

          ????public?Span?Span?{?get;?}

          ????public?Memory?Slice(int?start)
          ????public?Memory?Slice(int?start,?int?length)
          ????public?MemoryHandle?Pin()
          }

          使用也很簡單:

          byte[]?buffer?=?ArrayPool.Shared.Rent(16000?*?8);

          while?((bytesRead?=?await?fileStream.ReadAsync(buffer,?0,?buffer.Length))?>?0)
          {
          ????ParseBlock(new?ReadOnlyMemory(buffer,?start:?0,?length:?bytesRead));?
          }

          void?ParseBlock(ReadOnlyMemory?memory)
          {
          ????ReadOnlySpan?slice?=?memory.Span;
          }

          總結(jié)

          Span存在很長時(shí)間了,只是5.0做了一些優(yōu)化。

          用好了,對(duì)代碼是很好的補(bǔ)充和優(yōu)化,用不好,就會(huì)有給自己刨很多個(gè)坑。

          所以,耗子尾汁。


          往期精彩回顧




          【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程?★★★

          .NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃

          【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引

          Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)

          .NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了

          10個(gè)小技巧助您寫出高性能的ASP.NET Core代碼

          用abp vNext快速開發(fā)Quartz.NET定時(shí)任務(wù)管理界面

          在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度

          現(xiàn)身說法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化

          關(guān)于C#異步編程你應(yīng)該了解的幾點(diǎn)建議

          C#異步編程看這篇就夠了


          瀏覽 27
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲三级免费 | 日本精品在线视频 | 国产精品爽爽久久久久 | 一级不卡毛片 | 美女被c网站 |