Span這個東西出來好久了,竟然由於5.0又火起來了。html
在大多數狀況下,C#開發時,咱們只使用託管內存。而實際上,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回收成本也不同。爲防止非受權轉發,這兒給出本文的原文連接:http://www.javashuo.com/article/p-pdvtbvdg-nw.html數組
問個問題:寫了這麼多年的C#,咱們有用過指針嗎?有沒有想過爲何?緩存
咱們用個例子來回答這個問題:一個字符串,正常它是一個託管對象。安全
若是咱們想解析整個字符串,咱們會這麼寫:微信
int Parse(string managedMemory);
那麼,若是咱們想只解析一部分字符串,該怎麼寫?app
int Parse(string managedMemory, int startIndex, int length);
如今,咱們轉到非託管內存上:異步
unsafe int Parse(char* pointerToUnmanagedMemory, int length);
unsafe int Parse(char* pointerToUnmanagedMemory, int startIndex, int length);
再延伸一下,咱們寫幾個用於複製內存的功能:ide
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,須要設置開發語言爲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字段,它將被存儲在堆中。這是不容許的:
class Impossible
{
Span<byte> field;
}
不過,從C# 7.2開始,在其餘僅限堆棧的類型中有Span字段是能夠的:
ref struct TwoSpans<T>
{
public Span<T> first;
public Span<T> second;
}
接口實現意味着數據會被裝箱。而裝箱意味着存儲在堆中。同時,爲了防止裝箱,Span必須不實現任何現有的接口,例如最容易想到的IEnumerable
。也許某一天,C#會容許定義由結構體實現的結口?
異步在C#裏絕對是個好東西。
不過對於Span,是另外一件事。異步方法會建立一個AsyncMethodBuilder
構建器,構建器會建立一個異步狀態機。異步狀態機會將方法的參數放到堆上。因此,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能夠從託管數組、字符串或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 掃描二維碼,關注我的公衆號,能夠第一時間獲得最新的我的文章和內容推送 本文版權歸做者全部,轉載請保留此聲明和原文連接 |