Span這個東西出來很久了,居然因為5.0又火起來了,
?
相關知識
在大多數情況下,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回收成本也不一樣,
為防止非授權轉發,這兒給出本文的原文鏈接:https://www.cnblogs.com/tiger-wang/p/14029853.html
問題的產生
問個問題:寫了這么多年的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做了一些優化,
用好了,對代碼是很好的補充和優化,用不好,就會有給自己刨很多個坑,
所以,耗子尾汁,
![]() |
微信公眾號:老王Plus 掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送 本文著作權歸作者所有,轉載請保留此宣告和原文鏈接 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/227460.html
標籤:.NET技术
上一篇:關于C# Span的一些實踐
下一篇:求求大神指點

