C#如何安全、高效地玩轉(zhuǎn)任何種類的內(nèi)存之Span的本質(zhì)
C#如何安全、高效地玩轉(zhuǎn)任何種類的內(nèi)存之Span的本質(zhì)
一、what - 痛點(diǎn)是什么?
回答這個(gè)問題前,先總結(jié)一下如何用C#操作任何類型的內(nèi)存:
1、托管內(nèi)存(managed memory )
var mangedMemory = new Student();
很熟悉吧,只需使用new操作符就分配了一塊托管堆內(nèi)存,而且還不用手工釋放它,因?yàn)樗怯衫占鳎℅C)管理的,GC會智能地決定何時(shí)釋放它,這就是所謂的托管內(nèi)存。默認(rèn)情況下,GC通過復(fù)制內(nèi)存的方式分代管理小對象(size < 85000 bytes),而專門為大對象(size >= 85000 bytes)開辟大對象堆(LOH),管理大對象時(shí),并不會復(fù)制它,而是將其放入一個(gè)列表,提供較慢的分配和釋放,而且很容易產(chǎn)生內(nèi)存碎片。
2、棧內(nèi)存(stack memory )
unsafe{ var stackMemory = stackalloc byte[100]; }
很簡單,使用stackalloc關(guān)鍵字非??焖俚鼐头峙浜昧艘粔K棧內(nèi)存,也不用手工釋放,它會隨著當(dāng)前作用域而釋放,比如方法執(zhí)行結(jié)束時(shí),就自動釋放了。棧內(nèi)存的容量非常小( ARM、x86 和 x64 計(jì)算機(jī),默認(rèn)堆棧大小為 1 MB),當(dāng)你使用棧內(nèi)存的容量大于1M時(shí),就會報(bào)StackOverflowException 異常 ,這通常是致命的,不能被處理,而且會立即干掉整個(gè)應(yīng)用程序,所以棧內(nèi)存一般用于需要小內(nèi)存,但是又不得不快速執(zhí)行的大量短操作,比如微軟使用棧內(nèi)存來快速地記錄ETW事件日志。
3、本機(jī)內(nèi)存(native memory )
IntPtr nativeMemory0 = default(IntPtr), nativeMemory1 = default(IntPtr); try { unsafe { nativeMemory0 = Marshal.AllocHGlobal(256); nativeMemory1 = Marshal.AllocCoTaskMem(256); } } finally { Marshal.FreeHGlobal(nativeMemory0); Marshal.FreeCoTaskMem(nativeMemory1); }
通過調(diào)用方法Marshal.AllocHGlobal 或Marshal.AllocCoTaskMem 來分配非托管堆內(nèi)存,非托管就是垃圾回收器(GC)不可見的意思,并且還需要手工調(diào)用方法Marshal.FreeHGlobal or Marshal.FreeCoTaskMem 釋放它,千萬不能忘記,不然就內(nèi)存泄漏了。
二、拋磚引玉 - 痛點(diǎn)
首先我們設(shè)計(jì)一個(gè)解析完整或部分字符串為整數(shù)的API,如下:
public interface IntParser { // allows us to parse the whole string. int Parse(string managedMemory); // allows us to parse part of the string. int Parse(string managedMemory, int startIndex, int length); // allows us to parse characters stored on the unmanaged heap / stack. unsafe int Parse(char* pointerToUnmanagedMemory, int length); // allows us to parse part of the characters stored on the unmanaged heap / stack. unsafe int Parse(char* pointerToUnmanagedMemory, int startIndex, int length); }
從上面可以看到,為了支持解析來自任何內(nèi)存區(qū)域的字符串,一共寫了4個(gè)重載方法。
接下來在來設(shè)計(jì)一個(gè)支持復(fù)制任何內(nèi)存塊的API,如下:
public interface MemoryblockCopier { 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); }
腦袋蒙圈沒,以前C#操縱各種內(nèi)存就是這么復(fù)雜、麻煩。通過上面的總結(jié)如何用C#操作任何類型的內(nèi)存,相信大多數(shù)同學(xué)都能夠很好地理解這兩個(gè)類的設(shè)計(jì),但我心里是沒底的,因?yàn)槭褂昧瞬话踩a和指針,這些操作是危險(xiǎn)的、不可控的,根本無法獲得.net至關(guān)重要的安全保障,并且可能還會有難以預(yù)估的問題,比如堆棧溢出、內(nèi)存碎片、棧撕裂等等,微軟的工程師們早就意識到了這個(gè)痛點(diǎn),所以span誕生了,它就是這個(gè)痛點(diǎn)的解決方案。
三、how - span如何解決這個(gè)痛點(diǎn)?
先來看看,如何使用span操作各種類型的內(nèi)存(偽代碼):
1、托管內(nèi)存(managed memory )
var managedMemory = new byte[100]; Span<byte> span = managedMemory;
2、棧內(nèi)存(stack memory )
var stackedMemory = stackalloc byte[100]; var span = new Span<byte>(stackedMemory, 100);
3、本機(jī)內(nèi)存(native memory )
var nativeMemory = Marshal.AllocHGlobal(100); var nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100);
span就像黑洞一樣,能夠吸收來自于內(nèi)存任意區(qū)域的數(shù)據(jù),實(shí)際上,現(xiàn)在,在.Net的世界里,Span就是所有類型內(nèi)存的抽象化身,表示一段連續(xù)的內(nèi)存,它的API設(shè)計(jì)和性能就像數(shù)組一樣,所以我們完全可以像使用數(shù)組一樣地操作各種內(nèi)存,真的是太方便了。
現(xiàn)在重構(gòu)上面的兩個(gè)設(shè)計(jì),如下:
public interface IntParser { int Parse(Span<char> managedMemory); int Parse(Span<char>, int startIndex, int length); } public interface MemoryblockCopier { void Copy<T>(Span<T> source, Span<T> destination); void Copy<T>(Span<T> source, int sourceStartIndex, Span<T> destination, int destinationStartIndex, int elementsCount); }
上面的方法根本不關(guān)心它操作的是哪種類型的內(nèi)存,我們可以自由地從托管內(nèi)存切換到本機(jī)代碼,再切換到堆棧上,真正的享受玩轉(zhuǎn)內(nèi)存的樂趣。
四、why - 為什么span能解決這個(gè)痛點(diǎn)?
1、淺析span的工作機(jī)制
先來窺視一下源碼:
我已經(jīng)圈出的三個(gè)字段:偏移量、索引、長度(使用過ArraySegment<byte> 的同學(xué)可能已經(jīng)大致理解到設(shè)計(jì)的精髓了),這就是它的主要設(shè)計(jì),當(dāng)我們訪問span表示的整體或部分內(nèi)存時(shí),內(nèi)部的索引器會按照下面的算法運(yùn)算指針(偽代碼):
ref T this[int index] { get => ref ((ref reference + byteOffset) + index * sizeOf(T)); }
整個(gè)變化的過程,如圖所示:
上面的動畫非常清楚了吧,舊span整合它的引用和偏移成新的span的引用,整個(gè)過程并沒有復(fù)制內(nèi)存,也沒有返回相對位置上存在的副本,而是直接返回實(shí)際存儲位置的引用,因此性能非常高,因?yàn)樾聅pan獲得并更新了引用,所以垃圾回收器(GC)知道如何處理新的span,從而獲得了.Net至關(guān)重要的安全保障,并且內(nèi)部還會自動執(zhí)行邊界檢查確保內(nèi)存安全,而這些都是span內(nèi)部默默完成的,開發(fā)人員根本不用擔(dān)心,非托管世界依然美好。
正是由于span的高性能,目前很多基礎(chǔ)設(shè)施都開始支持span,甚至使用span進(jìn)行重構(gòu),比如:System.String.Substring方法,我們都知道此方法是非常消耗性能的,首先會創(chuàng)建一個(gè)新的字符串,然后再從原始字符串中復(fù)制字符集給它,而使用span可以實(shí)現(xiàn)Non-Allocating、Zero-coping,下面是我做的一個(gè)基準(zhǔn)測試:
使用String.SubString和Span.Slice分別截取長度為10和1000的字符串的前一半,從指標(biāo)Mean可以看出方法SubString的耗時(shí)隨著字符串長度呈線性增長,而Slice幾乎保持不變;從指標(biāo)Allocated Memory/Op可以看出,方法Slice并沒有被分配新的內(nèi)存,實(shí)踐出真知,可以預(yù)見Span未來將會成為.Net下編寫高性能應(yīng)用程序的重要積木,應(yīng)用前景也會非常地廣,微服務(wù)、物聯(lián)網(wǎng)、云原生都是它發(fā)光發(fā)熱的好地方。
從技術(shù)的本質(zhì)上看,Span<T>是一種ref-like type類似引用的結(jié)構(gòu)體;從應(yīng)用的場景上看,它是高性能的sliceable type可切片類型;綜上所訴,Span是一種類似于數(shù)組的結(jié)構(gòu)體,但具有創(chuàng)建數(shù)組一部分視圖,而無需在堆上分配新對象或復(fù)制數(shù)據(jù)的超能力。
補(bǔ)充:
可能會有的同學(xué)誤解了span,表面上認(rèn)為只是對指針的封裝,從而繞過unsafe帶來的限制,避免開發(fā)人員直接面對指針而已,其實(shí)不是,下面我們來看一個(gè)示例:
var nativeMemory = Marshal.AllocHGlobal(100); Span<byte> nativeSpan; unsafe { nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100); } SafeSum(nativeSpan); Marshal.FreeHGlobal(nativeMemory); // 這里不關(guān)心操作的內(nèi)存類型,即不用為一種類型寫一個(gè)重載方法,就好比上面的設(shè)計(jì)一樣。 static ulong SafeSum(Span<byte> bytes) { ulong sum = 0; for(int i=0; i < bytes.Length; i++) { sum += bytes[i]; } return sum; }
并沒有繞過unsafe,以前該如何用,現(xiàn)在還是一樣的,span解決的是下面幾點(diǎn):
- 高性能,避免不必要的內(nèi)存分配和復(fù)制。
- 高效率,它可以為任何具有無復(fù)制語義的連續(xù)內(nèi)存塊提供安全和可編輯的視圖,極大地簡化了內(nèi)存操作,即不用為每一種內(nèi)存類型操作寫一個(gè)重載方法。
- 內(nèi)存安全,span內(nèi)部會自動執(zhí)行邊界檢查來確保安全地讀寫內(nèi)存,但它并不管理如何釋放內(nèi)存,而且也管理不了,因?yàn)樗袡?quán)不屬于它,希望大家要明白這一點(diǎn)。
以上就是C#如何安全、高效地玩轉(zhuǎn)任何種類的內(nèi)存之Span的本質(zhì)的詳細(xì)內(nèi)容,更多關(guān)于C#語言的資料請關(guān)注腳本之家其它相關(guān)文章!,希望大家以后多多支持腳本之家!
相關(guān)文章
利用WinForm實(shí)現(xiàn)上左右布局的方法詳解
現(xiàn)在90%的管理系統(tǒng)都是在用上左右這種布局方式,真可謂是經(jīng)典永流傳。本文將利用WinForm實(shí)現(xiàn)上左右布局這一布局效果,感興趣的可以學(xué)習(xí)一下2022-09-09DataGridView設(shè)置單元格的提示內(nèi)容ToolTip
這篇文章介紹了DataGridView設(shè)置單元格提示內(nèi)容ToolTip的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02C#動態(tài)生成DropDownList執(zhí)行失敗原因分析
這篇文章主要介紹了C#動態(tài)生成DropDownList執(zhí)行失敗原因分析,以一個(gè)實(shí)例形式分析了C#動態(tài)生成DropDownList的相關(guān)注意要點(diǎn)與使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03C#實(shí)現(xiàn)文件操作(復(fù)制,移動,刪除)的方法詳解
File類提供了常見的文件操作函數(shù),包括復(fù)制、移動、刪除、創(chuàng)建快捷方式等,本文將通過一些簡單的示例為大家詳細(xì)講講具體的使用,希望對大家有所幫助2023-05-05