Span 全面介紹:探索 .NET 新增的重要組成部分
假設要公開特殊化排序例程,以就地對記憶體資料執行操作。可能要公開需要使用陣列的方法,並提供對相應 T[] 執行操作的實現。如果方法的呼叫方有陣列,且希望對整個陣列進行排序,這樣做就非常合適。但如果呼叫方只想對部分陣列進行排序,該怎麼辦?可能還要公開需要使用偏移和計數的過載。但如果要支援的記憶體資料不在陣列中,而是來自本機程式碼(舉個例子)或位於堆疊上,並且你只有指標和長度,該怎麼辦?如何才能讓編寫的排序方法對記憶體的任意區域執行操作,同時還對完整陣列或部分陣列以及託管陣列和非託管指標同樣有效?
又例如,假設要對 System.String 實現操作,如使用特殊化分析方法。可能要公開需要使用字串的方法,並提供對字串執行操作的實現。但如果要支援對部分字串執行操作,該怎麼辦?雖然 String.Substring 可用於分離出僅感興趣的部分,但此操作的成本相對高昂,涉及字串分配和記憶體複製。正如陣列示例中提到的,可以使用偏移和計數。但如果呼叫方沒有字串,而是有 char[],該怎麼辦?或者,如果呼叫方有 char*(例如為了使用堆疊上某空間而使用 stackalloc 建立的,或通過呼叫本機程式碼而生成的),該怎麼辦?如果才能讓編寫的分析方法不強制呼叫方執行任何分配或複製操作,同時還對輸入的型別字串、char[] 和 char* 同樣有效?
在這兩個示例中,都可以使用不安全程式碼和指標,同時公開接受指標和長度的實現。不過,這樣一來,就無法獲取對 .NET 至關重要的安全保障,並且會遇到對大多數 .NET 開發人員而言已成為過去的問題,如緩衝區溢位和訪問衝突。此外,這還會引發其他效能損失,如需要在操作期間固定託管物件,讓檢索的指標一直有效。而且根據涉及的資料型別,獲取指標根本就不可行。
此難題還是有解決方法的,即使用 Span<T>。
什麼是 Span<T>?
System.Span<T> 是在 .NET 中發揮關鍵作用的新值型別。使用它,可以表示任意記憶體的相鄰區域,無論相應記憶體是與託管物件相關聯,還是通過互操作由本機程式碼提供,亦或是位於堆疊上。除了具有上述用途外,它仍能確保安全訪問和高效能特性,就像陣列一樣。
例如,可以通過陣列建立 Span<T>:
var arr = new byte[10]; Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>
隨後,可以輕鬆高效地建立 Span,以利用 Span 的 Slice 方法過載,僅表示/指向此陣列的子集。隨後,可以為生成的 Span 編制索引,以編寫和讀取原始陣列中相關部分的資料:
Span<byte> slicedBytes = bytes.Slice(start: 5, length: 2); slicedBytes[0] = 42; slicedBytes[1] = 43; Assert.Equal(42, slicedBytes[0]); Assert.Equal(43, slicedBytes[1]); Assert.Equal(arr[5], slicedBytes[0]); Assert.Equal(arr[6], slicedBytes[1]); slicedBytes[2] = 44; // Throws IndexOutOfRangeExceptionbytes[2] = 45; // OK
Assert.Equal(arr[2], bytes[2]); Assert.Equal(45, arr[2]);
正如之前提到的,Span 不僅僅只能用於訪問陣列和分離出陣列子集。還可用於引用堆疊上的資料。例如,
Span<byte> bytes = stackalloc byte[2]; // Using C# 7.2 stackalloc support for spansbytes[0] = 42; bytes[1] = 43; Assert.Equal(42, bytes[0]); Assert.Equal(43, bytes[1]); bytes[2] = 44; // throws IndexOutOfRangeException
更為普遍的是,Span 可用於引用任意指標和長度(如通過本機堆分配的記憶體),如下所示:
IntPtr ptr = Marshal.AllocHGlobal(1);try{ Span<byte> bytes; unsafe { bytes = new Span<byte>((byte*)ptr, 1); } bytes[0] = 42; Assert.Equal(42, bytes[0]); Assert.Equal(Marshal.ReadByte(ptr), bytes[0]); bytes[1] = 43; // Throws IndexOutOfRangeException}finally { Marshal.FreeHGlobal(ptr); }
Span<T> 索引器利用 C# 7.0 中引入的 C# 語言功能,即引用返回。索引器使用“引用 T”返回型別進行宣告,其中提供為陣列編制索引的語義,同時返回對實際儲存位置的引用,而不是相應位置上存在的副本:
public ref T this[int index] { get { ... } }
通過示例,可以最明顯地體現這種引用返回型別索引器帶來的影響,如將它與不是引用返回型別的 List<T> 索引器進行比較。例如:
struct MutableStruct { public int Value; } ... Span<MutableStruct> spanOfStructs = new MutableStruct[1]; spanOfStructs[0].Value = 42; Assert.Equal(42, spanOfStructs[0].Value);var listOfStructs = new List<MutableStruct> { new MutableStruct() }; listOfStructs[0].Value = 42; // Error CS1612: the return value is not a variable
Span<T> 的第二個變體為 System.ReadOnlySpan<T>,可啟用只讀訪問。此型別與 Span<T> 基本類似,不同之處在於前者的索引器利用新 C# 7.2 功能來返回“引用只讀 T”,而不是“引用 T”,這樣就可以處理 System.String 等不可變資料型別。使用 ReadOnlySpan<T>,可以非常高效地分離字串,而無需執行分配或複製操作,如下所示:
string str = "hello, world";string worldString = str.Substring(startIndex: 7, length: 5); // Allocates ReadOnlySpan<char> worldSpan = str.AsReadOnlySpan().Slice(start: 7, length: 5); // No allocationAssert.Equal('w', worldSpan[0]); worldSpan[0] = 'a'; // Error CS0200: indexer cannot be assigned to
Span 的優勢還有許多,遠不止已提到的這些。例如,Span 支援 reinterpret_cast 的理念,即可以將 Span<byte> 強制轉換為 Span<int>(其中,Span<int> 中的索引 0 對映到 Span<byte> 的前四個位元組)。這樣一來,如果讀取位元組緩衝區,可以安全高效地將它傳遞到對分組位元組(視作整數)執行操作的方法。
如何實現 Span<T>?
開發人員通常無需瞭解要使用的庫是如何實現的。不過,對於 Span<T>,對背後的運作機制詳情至少有一個基本瞭解是值得的,因為這些詳情暗含有關效能和使用約束的相關資訊。
首先,Span<T> 是包含引用和長度的值型別,定義大致如下:
public readonly ref struct Span<T> { private readonly ref T _pointer; private readonly int _length; ... }
“引用 T”欄位這一概念初看起來有些奇怪,因為其實無法在 C# 或甚至 MSIL 中宣告“引用 T”欄位。不過,Span<T> 實際上旨在於執行時使用特殊內部型別,可看作是內部實時 (JIT) 型別,由 JIT 為其生成等效的“引用 T”欄位。以可能更為熟悉的引用用法為例:
public static void AddOne(ref int value) => value += 1; ...var values = new int[] { 42, 84, 126 }; AddOne(ref values[2]); Assert.Equal(127, values[2]);
此程式碼通過引用傳遞陣列中的槽,這樣(除優化外)還可以在堆疊上生成引用 T。Span<T> 中的引用 T 有異曲同工之妙,直接封裝在結構中。直接或間接包含此類引用的型別被稱為類似引用的型別,C# 7.2 編譯器支援在簽名中使用引用結構,從而宣告這種類似引用的型別。
根據這一簡要說明,應明確兩點:
Span<T> 的定義方式可確保操作效率與陣列一樣高:為 Span 編制索引無需通過計算來確定指標開頭及其起始偏移,因為“引用”欄位本身已對兩者進行了封裝。(相比之下,ArraySegment<T> 有單獨的偏移欄位,這就增加了索引編制和資料傳遞操作的成本。)
鑑於類似引用的型別這一本質,Span<T> 因其“引用 T”欄位而受到一些約束。
第二點帶來了一些有趣的後果,即導致 .NET 包含第二組相關的型別(由 Memory<T> 主導)。
什麼是 Memory<T>?為什麼需要它?
Span<T> 是類似引用的型別,因為它包含“引用”欄位,而且“引用”欄位不僅可以引用陣列等物件的開頭,還可以引用它們的中間部分:
var arr = new byte[100]; Span<byte> interiorRef1 = arr.AsSpan().Slice(start: 20); Span<byte> interiorRef2 = new Span<byte>(arr, 20, arr.Length – 20); Span<byte> interiorRef3 = Span<byte>.DangerousCreate(arr, ref arr[20], arr.Length – 20);
這些引用被稱為“內部指標”。對於 .NET 執行時的垃圾回收器,跟蹤這些指標是一項成本相對高昂的操作。因此,執行時將這些引用約束為僅存在於堆疊上,因為它隱式規定了可以存在的內部指標數量下限。
此外,如前所述,Span<T> 大於計算機的字大小;也就是說,對 Span 執行的讀取和寫入操作不是原子操作。如果多個執行緒同時對 Span 在堆上的欄位執行讀取和寫入操作,存在“撕裂”風險。 假設現有一個已初始化的 Span,其中包含有效引用和值為 50 的相應 _length。一個執行緒開始編寫新 Span,並且還編寫新 _pointer 值。然後,還未將相應的 _length 設定為 20,另一個執行緒就開始讀取 Span,其中包含新 _pointer 和更長的舊 _length。
這樣一來,Span<T> 示例只能存在於堆疊上,而不能存在於堆上。也就是說,無法將 Span 裝箱,進而無法將 Span<T> 與現有反射呼叫 API(舉個例子)結合使用,因為它們需要執行裝箱。這意味著,無法將 Span<T> 欄位封裝在類中,甚至也無法封裝在不類似引用的結構中。也就是說,如果 Span 可能會隱式成為類中的欄位,則無法使用它們。例如,將它們捕獲到 lambda 中,或將它們捕獲為非同步方法或迭代器中的本地欄位,因為這些本地欄位可能最終會成為編譯器生成的狀態機上的欄位。 這還意味著,無法將 Span<T> 用作泛型引數,因為型別引數例項可能最終會被裝箱或以其他方式儲存到堆上(暫無“where T : ref struct”約束)。
對於許多方案,尤其是對於受計算量限制和同步處理功能,這些限制無關緊要。不過,非同步功能卻是另一回事。無論是處理同步操作還是非同步操作,本文開頭提到的大部分有關陣列、陣列切片和本機記憶體等問題仍存在。但如果 Span<T> 無法儲存到堆,因而無法跨非同步操作暫留,那麼還有什麼解決方法?答案就是 Memory<T>。
Memory<T> looks very much like an ArraySegment<T>:public readonly struct Memory<T> { private readonly object _object; private readonly int _index; private readonly int _length; ... }
可以通過陣列建立 Memory<T>,並進行切片。這與處理 Span 基本相同,不同之處在於 Memory<T> 是不類似引用的結構,可以存在於堆上。然後,若要執行同步處理,可以從中獲取 Span<T>,例如:
static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream) { int bytesRead = await stream.ReadAsync(buffer); return Checksum(buffer.Span.Slice(0, bytesRead)); // Or buffer.Slice(0, bytesRead).Span}static int Checksum(Span<byte> buffer) { ... }
與 Span<T> 和 ReadOnlySpan<T> 一樣,Memory<T> 也有等效的只讀型別,即 ReadOnlyMemory<T>。與預期一樣,它的 Span 屬性返回 ReadOnlySpan<T>。請參閱圖 1,快速概覽在這些型別之間進行轉換的內建機制。
圖 1:在 Span 相關型別之間進行非分配/非複製轉換
來自 | 收件人 | 機制 |
ArraySegment<T> | Memory<T> | 隱式強制轉換、AsMemory 方法 |
ArraySegment<T> | ReadOnlyMemory<T> | 隱式強制轉換、AsReadOnlyMemory 方法 |
ArraySegment<T> | ReadOnlySpan<T> | 隱式強制轉換、AsReadOnlySpan 方法 |
ArraySegment<T> | Span<T> | 隱式強制轉換、AsSpan 方法 |
ArraySegment<T> | T[] | Array 屬性 |
Memory<T> | ArraySegment<T> | TryGetArray 方法 |
Memory<T> | ReadOnlyMemory<T> | 隱式強制轉換、AsReadOnlyMemory 方法 |
Memory<T> | Span<T> | Span 屬性 |
ReadOnlyMemory<T> | ArraySegment<T> | DangerousTryGetArray 方法 |
ReadOnlyMemory<T> | ReadOnlySpan<T> | Span 屬性 |
ReadOnlySpan<T> | ref readonly T | 索引器 get 取值函式、封送處理方法 |
Span<T> | ReadOnlySpan<T> | 隱式強制轉換、AsReadOnlySpan 方法 |
Span<T> | ref T | 索引器 get 取值函式、封送處理方法 |
字串 | ReadOnlyMemory<char> | AsReadOnlyMemory 方法 |
字串 | ReadOnlySpan<char> | 隱式強制轉換、AsReadOnlySpan 方法 |
T[] | ArraySegment<T> | 建構函式、隱式強制轉換 |
T[] | Memory<T> | 建構函式、隱式強制轉換、AsMemory 方法 |
T[] | ReadOnlyMemory<T> | 建構函式、隱式強制轉換、AsReadOnlyMemory 方法 |
T[] | ReadOnlySpan<T> | 建構函式、隱式強制轉換、AsReadOnlySpan 方法 |
T[] | Span<T> | 建構函式、隱式強制轉換、AsSpan 方法 |
void* | ReadOnlySpan<T> | 建構函式 |
void* | Span<T> | 建構函式 |
將會注意到,Memory<T> 的 _object 欄位並未強型別化為 T[],而是儲存為物件。這突出說明 Memory<T> 可以包裝陣列以外的內容,如 System.Buffers.OwnedMemory<T>。OwnedMemory<T> 是抽象類,可用於包裝需要密切管理其生存期的資料,如從池中檢索到的記憶體。此主題更為高階,超出了本文的介紹範圍,但這就是 Memory<T> 的用途所在(例如,用於將指標包裝到本機記憶體)。ReadOnlyMemory<char> 也可以與字串結合使用,就像 ReadOnlySpan<char> 一樣。
Span<T> 和 Memory<T> 如何與 .NET 庫整合?
在上面的 Memory<T> 程式碼片段中,將會注意到傳入 Memory<byte> 的 Stream.ReadAsync 呼叫。但如今在 .NET 中,Stream.ReadAsync 被定義為接受 byte[]。它的工作原理是什麼?
為了支援 Span<T> 及其成員,即將向 .NET 新增數百個新成員和型別。其中大多是現有基於陣列和基於字串的方法的過載,而另一些則是專注於特定處理方面的全新型別。例如,除了包含需要使用字串的現有過載外,所有原始型別(如 Int32)現在都包含接受 ReadOnlySpan<char> 的 Parse 過載。假設字串包含兩部分數字(用逗號隔開,如“123,456”),且希望分析這部分數字。現在,可以編寫如下程式碼:
string input = ...;int commaPos = input.IndexOf(',');int first = int.Parse(input.Substring(0, commaPos));int second = int.Parse(input.Substring(commaPos + 1));
不過,這會生成兩個字串分配。若要編寫高效能程式碼,兩個字串分配可能就太多了。此時,可以改為編寫如下程式碼:
string input = ...; ReadOnlySpan<char> inputSpan = input.AsReadOnlySpan();int commaPos = input.IndexOf(',');int first = int.Parse(inputSpan.Slice(0, commaPos));int second = int.Parse(inputSpan.Slice(commaPos + 1));
通過使用基於 Span 的新 Parse 過載,可以在這整個操作期間避免執行分配操作。類似分析和格式化方法可用於原始型別(如 Int32),其中包括 DateTime、TimeSpan 和 Guid 等核心型別,甚至還包括 BigInteger 和 IPAddress 等更高級別型別。
實際上,已跨框架添加了許多這樣的方法。從 System.Random 到 System.Text.StringBuilder,再到 System.Net.Socket,這些過載的新增有利於輕鬆高效地處理 {ReadOnly}Span<T> 和 {ReadOnly}Memory<T>。其中一些甚至帶來了額外的好處。例如,Stream 現包含以下方法:
public virtual ValueTask<int> ReadAsync( Memory<byte> destination, CancellationToken cancellationToken = default) { ... }
將會注意到,不同於接受 byte[] 並返回 Task<int> 的現有 ReadAsync 方法,此過載不僅接受 Memory<byte>(而不是 byte[]),還返回 ValueTask<int>(而不是 Task<int>)。在以下情況下,ValueTask<T> 是有助於避免執行分配操作的結構:經常要求使用非同步方法來同步返回內容,以及不太可能為所有常見返回值快取已完成任務。例如,執行時可以為結果 true 和 false 快取已完成的 Task<bool>,但無法為 Task<int> 的所有可能結果值快取四十億任務物件。
由於相當常見的是 Stream 實現的緩衝方式讓 ReadAsync 呼叫同步完成,因此這一新 ReadAsync 過載返回 ValueTask<int>。也就是說,同步完成的非同步 Stream 讀取操作可以完全避免執行分配操作。ValueTask<T> 也用於其他新過載,如 Socket.ReceiveAsync、Socket.SendAsync、WebSocket.ReceiveAsync 和 TextReader.ReadAsync 過載。
此外,在一些情況下,Span<T> 還支援向框架新增在過去引發記憶體安全問題的方法。假設要建立的字串包含隨機生成的值(如某類 ID)。現在,可能會編寫要求分配字元陣列的程式碼,如下所示:
int length = ...; Random rand = ...;var chars = new char[length];for (int i = 0; i < chars.Length; i++) { chars[i] = (char)(rand.Next(0, 10) + '0'); }string id = new string(chars);
可以改用堆疊分配,甚至能夠利用 Span<char>,這樣就無需使用不安全程式碼。此方法還利用接受 ReadOnlySpan<char> 的新字串建構函式,如下所示:
int length = ...; Random rand = ...; Span<char> chars = stackalloc char[length];for (int i = 0; i < chars.Length; i++) { chars[i] = (char)(rand.Next(0, 10) + '0'); }string id = new string(chars);
這樣做更好,因為避免了堆分配,但仍不得不將堆疊上生成的資料複製到字串中。同樣,只有在所需空間大小對於堆疊而言足夠小時,此方法才有效。如果長度較短(如 32 個位元組),可以使用此方法;但如果長度為數千位元組,很容易就會引發堆疊溢位問題。如果可以改為直接寫入字串的記憶體,該怎麼辦?Span<T> 可以實現此目的。除了包含新建構函式以外,字串現在還包含 Create 方法:
public static string Create<TState>( int length, TState state, SpanAction<char, TState> action); ...public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);
實現此方法是為了分配字串,並分發可寫 Span,執行寫入操作後可以在構造字串的同時填寫字串的內容。請注意,在此示例中,Span<T> 的僅限堆疊這一本質非常有用,因為可以保證在字串的建構函式完成前 Span(引用字串的內部儲存)就不存在,這樣便無法在構造完成後使用 Span 改變字串了:
int length = ...; Random rand = ...;string id = string.Create(length, rand, (Span<char> chars, Random r) => { for (int i = 0; chars.Length; i++) { chars[i] = (char)(r.Next(0, 10) + '0'); } });
現在,不僅避免了分配操作,還可以直接寫入字串在堆上的記憶體,即也避免了複製操作,且不受堆疊大小限制的約束。
除了核心框架型別有新成員外,我們還正在積極開發許多可與 Span 結合使用的新 .NET 型別,從而在特定方案中實現高效處理。例如,對於要編寫高效能微服務和處理大量文字的網站的開發人員,如果在使用 UTF-8 時無需編碼和解碼字串,則效能會大大提升。為此,我們即將新增 System.Buffers.Text.Base64、System.Buffers.Text.Utf8Parser 和 System.Buffers.Text.Utf8Formatter 等新型別。這些型別對位元組 Span 執行操作,不僅避免了 Unicode 編碼和解碼,還能夠處理在各種網路堆疊的最低級別中常見的本機緩衝:
ReadOnlySpan<byte> utf8Text = ...;if (!Utf8Parser.TryParse(utf8Text, out Guid value, out int bytesConsumed, standardFormat = 'P')) throw new InvalidDataException();
所有此類功能不僅僅只用於公共使用用途;框架本身也可以利用這些基於 Span<T> 和基於 Memory<T> 的新方法來提升效能。跨 .NET Core 呼叫網站已切換為使用新的 ReadAsync 過載,以避免不必要的分配操作。分析過去是通過分配子字串完成,現在可以避免執行分配操作。甚至 Rfc2898DeriveBytes 等間隙型別也實際運用了此功能,利用 System.Security.Cryptography.HashAlgorithm 上基於 Span<byte> 的新 TryComputeHash 方法顯著減少分配操作量(每次演算法迭代的位元組陣列,可能迭代數千次)和提升吞吐量。
這並未止步於核心 .NET 庫一級,而是繼續全面影響堆疊。ASP.NET Core 現在嚴重依賴 Span;例如,在 Span 基礎之上編寫 Kestrel 伺服器的 HTTP 分析程式。Span 今後可能會通過較低級別 ASP.NET Core 中的公共 API 公開,如在它的中介軟體管道中。
.NET 執行時又如何呢?
.NET 執行時提供安全保障的方法之一是,確保為陣列編制的索引不超出陣列的長度,這種做法稱為“邊界檢查”。例如,以下面這個方法為例:
[MethodImpl(MethodImplOptions.NoInlining)]static int Return4th(int[] data) => data[3];
在我撰寫本文使用的 x64 計算機上,針對此方法生成的程式集如下所示:
sub rsp, 40 cmp dword ptr [rcx+8], 3 jbe SHORT G_M22714_IG04 mov eax, dword ptr [rcx+28] add rsp, 40 ret G_M22714_IG04: call CORINFO_HELP_RNGCHKFAIL int3
cmp 指令將資料陣列的長度與索引 3 進行比較。如果 3 超出範圍(異常丟擲),後續 jbe 指令會轉到範圍檢查失敗例程。雖然 JIT 需要生成程式碼,以確保此類訪問不會超出陣列邊界,但這並不意味著每個陣列訪問都需要進行邊界檢查。以下面的 Sum 方法為例:
static int Sum(int[] data) { int sum = 0; for (int i = 0; i < data.Length; i++) sum += data[i]; return sum; }
雖然 JIT 此時需要生成程式碼,以確保對 data[i] 的訪問不超出陣列邊界,但因為 JIT 能夠通過迴圈結構判斷 i 一直在範圍內(迴圈從頭到尾遍歷每個元素),所以 JIT 可以優化為不對陣列進行邊界檢查。因此,針對迴圈生成的程式集程式碼如下所示:
G_M33811_IG03: movsxd r9, edx add eax, dword ptr [rcx+4*r9+16] inc edx cmp r8d, edx jg SHORT G_M33811_IG03
雖然 cmp 指令仍在迴圈中,但只需將 i 值(儲存在 edx 暫存器中)與陣列長度(儲存在 r8d 暫存器中)進行比較,無需額外進行邊界檢查。
執行時向 Span(Span<T> 和 ReadOnlySpan<T>)應用類似優化。將上面的示例與下面的程式碼進行比較,唯一的變化是引數型別:
static int Sum(Span<int> data) { int sum = 0; for (int i = 0; i < data.Length; i++) sum += data[i]; return sum; }
針對此程式碼生成的程式集幾乎完全相同:
G_M33812_IG03: movsxd r9, r8d add ecx, dword ptr [rax+4*r9] inc r8d cmp r8d, edx jl SHORT G_M33812_IG03
程式集程式碼如此相似,部分是因為不用進行邊界檢查。此外,同樣重要的是 JIT 將 Span 索引器識別為內部型別,即 JIT 為索引器生成特殊程式碼,而不是將它的實際 IL 程式碼轉換為程式集。
所有這些都是為了說明執行時可以為 Span 應用與陣列相同的優化型別,讓 Span 成為高效的資料訪問機制。如需瞭解更多詳情,請參閱 bit.ly/2zywvyI 上的部落格文章。
C# 語言和編譯器又如何呢?
我已暗示,新增到 C# 語言和編譯器的功能有助於讓 Span<T> 成為 .NET 中的一流成員。C# 7.2 的多項功能都與 Span 相關(實際上,C# 7.2 編譯器必須使用 Span<T>)。接下來,將介紹三個此類功能。
引用結構。如前所述,Span<T> 是類似引用的型別,自版本 7.2 起在 C# 中公開為引用結構。通過將引用關鍵字置於結構前,可以指示 C# 編譯器將其他引用結構型別(如 Span<T>)用作欄位,這樣做還會註冊要分配給型別的相關約束。例如,若要為 Span<T> 編寫結構列舉器,列舉器需要儲存 Span<T>,因此它本身必須是引用結構,如下所示:
public ref struct Enumerator { private readonly Span<char> _span; private int _index; ... }
Span 的 stackalloc 初始化。在舊版 C# 中,只能將 stackalloc 的結果儲存到指標本地變數中。自 C# 7.2 起,現在可以在表示式中使用 stackalloc,並能定目標到 Span,而不使用不安全關鍵字。因為,無需編寫:
Span<byte> bytes;unsafe{ byte* tmp = stackalloc byte[length]; bytes = new Span<byte>(tmp, length); }
只需編寫:
Span<byte> bytes = stackalloc byte[length];
如果需要一些空間來執行操作,但又希望避免分配相對較小的堆記憶體,此程式碼就非常有用。過去有以下兩種選擇:
編寫兩個完全不同的程式碼路徑,對基於堆疊的記憶體和基於堆的記憶體執行分配和操作。
固定與託管分配相關聯的記憶體,再委託到實現,實現也用於基於堆疊的記憶體,並通過不安全程式碼中的指標控制進行編寫。
現在,不使用程式碼複製,即可完成相同的操作,而且還可以使用安全程式碼和最簡單的操作:
Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length]; ... // Code that operates on the Span<byte>
Span 使用驗證。因為 Span 可以引用可能與給定堆疊幀相關聯的資料,所以傳遞 Span 可能存在危險,此操作可能會引用不再有效的記憶體。例如,假設方法嘗試執行以下操作:
static Span<char> FormatGuid(Guid guid) { Span<char> chars = stackalloc char[100]; bool formatted = guid.TryFormat(chars, out int charsWritten, "d"); Debug.Assert(formatted); return chars.Slice(0, charsWritten); // Uh oh}
此時,空間從堆疊進行分配,然後嘗試返回對此空間的引用,但在返回的同時,此空間不再可用。幸運的是,C# 編譯器使用引用結構檢測此類無效使用,並會停止編譯,同時顯示以下錯誤:
錯誤 CS8352:無法在此上下文中使用本地“字元”,因為它可能會在聲明範圍外公開引用的變數
接下來會怎樣呢?
本文介紹的型別、方法、執行時優化和其他元素即將順利新增到 .NET Core 2.1 中。之後,我預計它們會全面影響 .NET Framework。核心型別(如 Span<T>)和新型別(如 Utf8Parser)也即將順利新增到與 .NET Standard 1.1 相容的 System.Memory.dll 包中。這樣一來,相關功能將適用於現有 .NET Framework 和 .NET Core 版本,儘管在內置於平臺時沒有實現一些優化。現在,可以試用此包的預覽版,只需新增對 NuGet 上 System.Memory.dll 包的引用即可。
當然,請注意,當前預覽版與實際釋出的穩定版之間可能會有重大變革。此類變革很大程度上源於像你這樣的開發人員在試用功能集時提供的反饋。因此,請試用預覽版,並關注 github.com/dotnet/coreclr 和 github.com/dotnet/corefx 儲存庫,以掌握最新動態。此外,有關文件,還可以訪問 aka.ms/ref72。
總的來說,此功能集能否取得成功依賴開發人員試用預覽版、提供反饋以及利用這些型別生成自己的庫,所有這些都是為了能夠在新式 .NET 程式中高效安全地訪問記憶體。我們熱切期待聆聽大家的使用體驗反饋,最好能夠與大家一起在 GitHub 上進一步改進 .NET。
原文:https://msdn.microsoft.com/zh-cn/magazine/mt814808
.NET社群新聞,深度好文,歡迎訪問公眾號文章彙總 http://www.csharpkit.com
相關推薦
Span 全面介紹:探索 .NET 新增的重要組成部分
假設要公開特殊化排序例程,以就地對記憶體資料執行操作。可能要公開需要使用陣列的方法,並提供對相應
C++:探索std::map和std::unordered_map中的新增操作
std::map和std::unordered_map主要提供如下幾種新增操作: try_emplace () (C++17) emplace () insert() [] = 下面給出一段測試程式碼,觀察物件在新增到std::map中時,
Omnigraffle中文版:物件檢查器全面介紹
Omnigraffle中文版雖然是一款繪圖軟體,但是它對於文字的處理功能也很強大,在Omnigraffle物件檢查器裡您可以對圖形、線條、文字的屬性進行設定,下面就為大傢俱體介紹一下Omnigraffle的物件檢查器。 首先開啟軟體,我們要找到物件檢查器就要先
C++:探索std::map和std::unordered_map中最高效的新增操作
std::map和std::unordered_map主要提供如下幾種新增操作: try_emplace () (C++17) emplace () insert() 下面給出一段測試程式碼,觀察物件在新增到std::map中時,構造物件過程中會有什麼區別: #i
筆記三:ASP.NET MVC 新增一個新頁面,執行顯示HTTP 404。您正在查詢的資源(或者它的一個依賴項)可能已被移除,或其名稱已更改,或暫時不可用。請檢查以下 URL 並確保其拼寫正確。
原操作:直接View下對應資料夾中新增頁面,執行時報錯。解決方案:1.刪除之前建立的頁面,然後找到該資料夾對應的控制器Controller。2.新增以下: public ActionResult path_show() { return V
【無私分享:ASP.NET CORE 專案實戰(第九章)】建立區域Areas,新增TagHelper
目錄索引 簡介 在Asp.net Core VS2015中,我們發現還有很多不太簡便的地方,比如右擊新增檢視,轉到試圖頁等功能圖不見了,雖然我們可以通過工具欄的自定義命令,把這兩個右擊選單新增上,但是貌似是灰色的不能用。 其實,這樣也好,通過手動建立,更讓我們深刻的理解M
【無私分享:ASP.NET CORE 專案實戰(第二章)】新增EF上下文物件,新增介面、實現類以及無處不在的依賴注入(DI)
目錄索引 簡介 上一章,我們介紹了安裝和新建控制器、檢視,這一章我們來建立個數據模型,並且新增介面和實現類。 新增EF上下文物件 按照我們以前的習慣,我們還是新建幾個資料夾 Commons:存放幫助類 Domians:資料模型 Services
【無私分享:ASP.NET CORE 專案實戰(第十二章)】新增對SqlServer、MySql、Oracle的支援
目錄索引 簡介 增加對多資料庫的支援,並不是意味著同時對多種資料庫操作,當然,後面,我們會嘗試同時對多種資料庫操作,這可能需要多個上下文,暫且不論。分散式資料庫,我們採用的是阿里雲的Mycat,這個後面會更新出來。我們今天的場景是:我們的專案可能是在windows上開發的使用的
【Android Studio探索之路系列】之六:Android Studio新增依賴
【Android Studio探索之路系列】章節列表 本文主要講解如何在Android Studio中新增JAR包、Module和SO庫。 一 JAR包的依賴配置 首先使用快捷鍵Ctrl+Alt+Shift+S開啟當前專案的配置,如下圖
asp.net夜話之七:ADO.NET介紹
Asp.net夜話之七:ADO.NET介紹ADO.NET是對Microsoft ActiveX Data Objects (ADO)一個跨時代的改進,它提供了平臺互用性和可伸縮的資料訪問。由於傳送的資料都是XML格式的,因此任何能夠讀取XML格式的應用程式都可以進行資料處
全面介紹Windows記憶體管理機制及C++記憶體分配例項(一):程序空間
本文背景: 在程式設計中,很多Windows或C++的記憶體函式不知道有什麼區別,更別談有效使用;根本的原因是,沒有清楚的理解作業系統的記憶體管理機制,本文企圖通過簡單的總結描述,結合例項來闡明這個機制。 本文目的: 對Windows記憶體管理機制瞭解清楚,有效的利用C++
CNN結構:SPP-Net為CNNs新增空間尺度卷積-神經元層
前幾個CNN檢測的框架要求網路的影象輸入為固定長寬,而SPP-Net在CNN結構中添加了一個實現影象金字塔功能的卷積層SPP層,用於在網路中實現多尺度卷積,由此對應多尺度輸入,以此應對影象的縮放變換和仿射變換。 一、文章的主要思想 考慮
.NET(C#、VB)APP開發——Smobiler平臺控制元件介紹:SliderView控制元件
SliderView控制元件 一、  
haproxy 負載均衡算法介紹:
haproxy 負載均衡 算法 一、Haproxy配置介紹:配置文件:/usr/local/haproxy/etc/haproxy.cfgbalance roundrobin # 負載均衡算法配置二、Haproxy負載均衡算法介紹:balance roundrobin # 輪詢,軟負載
ASP.NET MVC5(一):ASP.NET MVC概覽
depend 靈活 預覽版 管理 res lob 代碼 oba 引擎 ASP.NET MVC概覽 ASP.NET MVC是一種構建Web應用程序的框架,它將一般的MVC(Model-View-Controller)模式應用於ASP.NET框架。 ASP.NET MVC模式
微軟公告:ASP.NET曝漏洞Win7等均中招
j2e 漏洞 py3 fyi k60 mst asp.net avi box GNOME3.2%E6%AD%A3%E5%BC%8F%E7%89%88%E5%8F%91%E5%B8%83 http://music.baidu.com/songlist/495669502?1
.net core 2.0學習筆記(四):遷移.net framework 工程到.net core
編譯 its evel hashtable ref 學習筆記 inline null 創建 在遷移.net core的過程中,第一步就是要把.net framework 工程的目標框架改為.net core2.0,但是官網卻沒有提供轉換工具,需要我們自己動手完成了
Angular4.0踩坑之路:探索子路由和懶加載
ati clas per 而是 配置 trap child property one 參考文章: Angular4路由快速入門 http://www.jianshu.com/p/e72c79c6968e Angular2文檔學習的知識點摘要——Angular模塊(NgMo
[dotnetCore2.0]學習筆記之一: ASP.NET Core 升級到2.0
玩耍 後來 razor ons 引用 net ins install 查找 需要升級: 1、SDK2.0 ,需要單獨安裝;https://www.microsoft.com/net/core#windowscmd VS2017 不包含這個SDK;而這個SDK包含了run
OCR文字識別軟件:數字信息化不可或缺的重要組成部分
orien 生產 結果 復制粘貼 自學 文檔 能夠 字符 結構 OCR文字識別技術,是在國家“863”計劃國家自然科學基金長期支持下,清華大學電子工程系智能圖文信息處理研究室漢字識別研究工作的基礎上開發完成的。該軟件能夠快速地將印刷的文檔轉化為可供閱讀和可編輯的高質量電子文