假設要公開特殊化排序例程,以就地對內存數據執行操做。可能要公開須要使用數組的方法,並提供對相應 T[] 執行操做的實現。若是方法的調用方有數組,且但願對整個數組進行排序,這樣作就很是合適。但若是調用方只想對部分數組進行排序,該怎麼辦?可能還要公開須要使用偏移和計數的重載。但若是要支持的內存數據不在數組中,而是來自本機代碼(舉個例子)或位於堆棧上,而且你只有指針和長度,該怎麼辦?如何才能讓編寫的排序方法對內存的任意區域執行操做,同時還對完整數組或部分數組以及託管數組和非託管指針一樣有效?git
又例如,假設要對 System.String 實現操做,如使用特殊化分析方法。可能要公開須要使用字符串的方法,並提供對字符串執行操做的實現。但若是要支持對部分字符串執行操做,該怎麼辦?雖然 String.Substring 可用於分離出僅感興趣的部分,但此操做的成本相對高昂,涉及字符串分配和內存複製。正如數組示例中提到的,可使用偏移和計數。但若是調用方沒有字符串,而是有 char[],該怎麼辦?或者,若是調用方有 char*(例如爲了使用堆棧上某空間而使用 stackalloc 建立的,或經過調用本機代碼而生成的),該怎麼辦?若是才能讓編寫的分析方法不強制調用方執行任何分配或複製操做,同時還對輸入的類型字符串、char[] 和 char* 一樣有效?github
在這兩個示例中,均可以使用不安全代碼和指針,同時公開接受指針和長度的實現。不過,這樣一來,就沒法獲取對 .NET 相當重要的安全保障,而且會遇到對大多數 .NET 開發人員而言已成爲過去的問題,如緩衝區溢出和訪問衝突。此外,這還會引起其餘性能損失,如須要在操做期間固定託管對象,讓檢索的指針一直有效。並且根據涉及的數據類型,獲取指針根本就不可行。算法
此難題仍是有解決方法的,即便用 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 IndexOutOfRangeException bytes[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 spans bytes[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」返回類型進行聲明,其中提供爲數組編制索引的語義,同時返回對實際存儲位置的引用,而不是相應位置上存在的副本:dom
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 allocation Assert.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> 是包含引用和長度的值類型,定義大體以下:
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 編譯器支持在簽名中使用引用結構,從而聲明這種相似引用的類型。
根據這一簡要說明,應明確兩點:
第二點帶來了一些有趣的後果,即致使 .NET 包含第二組相關的類型(由 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> 同樣。
在上面的 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.Base6四、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 運行時提供安全保障的方法之一是,確保爲數組編制的索引不超出數組的長度,這種作法稱爲「邊界檢查」。例如,如下面這個方法爲例:
[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# 語言和編譯器的功能有助於讓 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。
Stephen Toub 就任於 Microsoft,負責 .NET 產品。能夠在 GitHub (github.com/stephentoub) 上關注他。
本文轉自 https://msdn.microsoft.com/zh-cn/magazine/mt814808