日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > C# >内容正文

C#

关于C# Span的一些实践

發布時間:2023/12/4 C# 58 豆豆
生活随笔 收集整理的這篇文章主要介紹了 关于C# Span的一些实践 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Span這個東西出來很久了,居然因為5.0又火起來了。

特別感謝RC兄弟提出這個話題。

?

相關知識

在大多數情況下,C#開發時,我們只使用托管內存。而實際上,C#為我們提供了三種類型的內存:

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

  • 非托管內存 - 通過Marshal.AllocHGlobal或xMarshal.AllocCoTaskMem方法分配在非托管堆上的內存。這個內存對GC不可見,并且必須通過Marshal.FreeHGlobal或Marshal.FreeCoTaskMem的顯式調用來釋放。使用非托管內存,最主要的目的是不給GC增加額外的壓力,所以最經常的使用方式是在分配大量沒有指針的值類型時使用。在Kestrel的代碼中,很多地方用到了非托管內存。

  • 托管內存 - 大多數代碼中最常用的內存,需要用new操作符來分配。之所以稱為托管(managed),因為它是被GC(垃圾管理器)管理的,由GC決定何時釋放內存,而不需要開發人員考慮。GC又將托管對象根據大小(85000字節)分為大對象和小對象。兩個對象的分配方式、速度和位置都有不同,小對象相對快點,大對象相對慢點。另外,兩種對象的GC回收成本也不一樣。

問題的產生

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

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

如果我們想解析整個字符串,我們會這么寫:

int?Parse(string?managedMemory);

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

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

現在,我們轉到非托管內存上:

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

再延伸一下,我們寫幾個用于復制內存的功能:

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

是不是很復雜?而且看上去并不安全?

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

Span

在定義中,Span就是一個簡單的值類型。它真正的價值,在于允許我們與任何類型的連續內存一起工作。

這些所謂的連續內存,包括:

  • 非托管內存緩沖區

  • 數組和子串

  • 字符串和子字符串

在使用中,Span確保了內存和數據安全,而且幾乎沒有開銷。

使用Span

要使用Span,需要設置開發語言為C# 7.2以上,并引用System.Memory到項目。

<PropertyGroup><LangVersion>7.2</LangVersion> </PropertyGroup>

使用低版本編譯器,會報錯:Error CS8107 Feature 'ref structs' is not available in C# 7.0. Please use language version 7.2 or greater.。

?

Span使用時,最簡單的,可以把它想象成一個數組,它會做所有的指針運算,同時,內部又可以指向任何類型的內存。

例如,我們可以為非托管內存創建Span:

Span<byte>?stackMemory?=?stackalloc?byte[256];IntPtr?unmanagedHandle?=?Marshal.AllocHGlobal(256); Span<byte>?unmanaged?=?new?Span<byte>(unmanagedHandle.ToPointer(),?256);? Marshal.FreeHGlobal(unmanagedHandle);

從T[]到Span的隱式轉換:

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

?

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

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

?

Span創建完成后,就跟普通的數組一樣,有一個Length屬性和一個允許讀寫的index,因此使用時就和一般的數組一樣使用就好。

看看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<T>?Slice(int?start); Span<T>?Slice(int?start,?int?length);void?Clear(); void?Fill(T?value);void?CopyTo(Span<T>?destination); bool?TryCopyTo(Span<T>?destination);

?

我們用Span來實現一下文章開頭的復制內存的功能:

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

看看,是不是非常簡單?

而且,使用Span時,運行性能極佳。關于Span的性能,網上有很多評測,關注的兄弟可以自己去看。

Span的限制

Span支持所有類型的內存,所以,它也會有相當嚴格的限制。

在上面的例子中,使用的是堆棧內存。所有指向堆棧的指針都不能存儲在托管堆上。因為方法結束時,堆棧會被釋放,指針會變成無效值,如果再使用,就是內存溢出。

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

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

  • 如果在類中設置Span字段,它將被存儲在堆中。這是不允許的:

    class?Impossible {Span<byte>?field; }

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

    ref?struct?TwoSpans<T> {public?Span<T>?first;public?Span<T>?second; }?
  • Span不能有接口實現

  • 接口實現意味著數據會被裝箱。而裝箱意味著存儲在堆中。同時,為了防止裝箱,Span必須不實現任何現有的接口,例如最容易想到的IEnumerable。也許某一天,C#會允許定義由結構體實現的結口?

  • Span不能是異步方法的參數

  • 異步在C#里絕對是個好東西。

    不過對于Span,是另一件事。異步方法會創建一個AsyncMethodBuilder構建器,構建器會創建一個異步狀態機。異步狀態機會將方法的參數放到堆上。所以,Span不能用作異步方法的參數。

  • Span不能是泛型的代入參數

  • 看下面的代碼:

    Span<byte>?Allocate()?=>?new?Span<byte>(new?byte[256]);void?CallAndPrint<T>(Func<T>?valueProvider)? {object?value?=?valueProvider.Invoke();Console.WriteLine(value.ToString()); }void?Demo() {Func<Span<byte>>?spanProvider?=?Allocate;CallAndPrint<Span<byte>>(spanProvider); }

    同樣也是裝箱的原因。

    ?

    上面是Span的內容。

    下面簡單說一下另一個經常跟Span一起提的內容:Memory

    Memory

    Memory是一個新的數據類型,它只能指向托管內存,所以不具有僅限堆棧的限制。

    Memory可以從托管數組、字符串或IOwnedMemory中創建,傳遞給異步方法或存儲在類的字段中。當需要Span時,就調用它的Span屬性。它會根據需要創建Span。然后在當前范圍內使用它。

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

    public?readonly?struct?Memory<T> {private?readonly?object?_object;private?readonly?int?_index;private?readonly?int?_length;public?Span<T>?Span?{?get;?}public?Memory<T>?Slice(int?start)public?Memory<T>?Slice(int?start,?int?length)public?MemoryHandle?Pin() }

    使用也很簡單:

    byte[]?buffer?=?ArrayPool<byte>.Shared.Rent(16000?*?8);while?((bytesRead?=?await?fileStream.ReadAsync(buffer,?0,?buffer.Length))?>?0) {ParseBlock(new?ReadOnlyMemory<byte>(buffer,?start:?0,?length:?bytesRead));? }void?ParseBlock(ReadOnlyMemory<byte>?memory) {ReadOnlySpan<byte>?slice?=?memory.Span; }

    總結

    Span存在很長時間了,只是5.0做了一些優化。

    用好了,對代碼是很好的補充和優化,用不好,就會有給自己刨很多個坑。

    所以,耗子尾汁。

    喜歡就來個三連,讓更多人因你而受益

    總結

    以上是生活随笔為你收集整理的关于C# Span的一些实践的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。