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

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.AllocHGlobal或xMarshal.AllocCoTaskMem方法分配在非托管堆上的內(nèi)存。這個(gè)內(nèi)存對(duì)GC不可見,并且必須通過Marshal.FreeHGlobal或Marshal.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í)例也不能駐留在托管堆上,而只能駐留在堆棧上。這又引出一些限制。
Span不能是非堆棧類型的字段
如果在類中設(shè)置Span字段,它將被存儲(chǔ)在堆中。這是不允許的:
class?Impossible
{
????Span?field;
}
不過,從C# 7.2開始,在其他僅限堆棧的類型中有Span字段是可以的:
ref?struct?TwoSpans
{
????public?Span?first;
????public?Span?second;
}?
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é)口?
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ù)。
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)化
