日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

Span<T> —— .NET Core 高效运行的新基石

發布時間:2024/3/12 asp.net 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Span<T> —— .NET Core 高效运行的新基石 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文:https://msdn.microsoft.com/en-us/magazine/mt814808.aspx

目錄

Span 是什么鬼?

Span 是如何實現的?

Memory 又是什么鬼?

Span 和 Memory 是如何與 .NET 庫集成的?

.NET 運行時有何變化?

C# 語言及其編譯器有啥變化?

接下來呢?


假定我們想要寫一個方法,來對內存中的數據進行排序。你可能會為該方法提供一個 T [ ]?數組參數。如果調用者想對整個數組進行排序,這個方法就沒有問題,但是如果調用者只想要對數組的一部分進行排序呢?然后,你可能還會暴露一個帶 offset 和 count 的重載。但是,如果你想讓這個排序方法不僅支持數組,也支持本機代碼(例如一個數組在堆棧中,我們只有一個指針和長度信息),你怎么編寫這個排序方法,它可以在任意內存區域上運行,既支持完整的數組,也支持數組的子集,既能處理管數組,也能處理非托管指針?

再看一個例子。假如我們需要為 System.String?類寫一個解析方法。你可能會編寫一個接受字符串參數并操作該字符串的方法。但是,如果想對該字符串的子集進行操作,該怎么辦? 我們可以用 String.Substring 來抽取,但這是一個昂貴的操作,涉及字符串分配和內存復制。我們像按照上個例子那樣,取一個偏移量和一個計數,但是如果調用者沒有字符串而是有一個 char [] ?會怎樣?再或者,如果調用者有一個 char *(比如他們用 stackalloc?創建的來使用堆棧上的一些空間,或者是調用本機代碼獲得的結果),該怎么辦呢?你怎么能在不強迫調用者進行任何分配或復制的情況下,使用你的方法,并對 string,char []?和 char *?類型的輸入同樣有效?

在這兩種情況下,你可以使用不安全的代碼和指針,接受指針和長度作為參數。但是,這繞過了.NET 的核心安全保障,可能造成緩沖區溢出和訪問沖突等問題,這些問題對于大多數.NET開發人員來說已成為過去。它還會產生額外的性能損失,例如需要在操作期間固定托管對象,以便指針保持有效。根據所涉及的數據類型,獲取指針可能并不實際。

這個難題有一個答案,它的名字是 Span <T>。

Span<T> 是什么鬼?

System.Span<T> 是核心 .NET 庫提供 的一個新的值類型。它代表著一塊已知長度的連續內存塊,這個內存塊可以關聯到一個托管對象,可以是通過互操作獲取的本機碼,也可以是棧的一部分。它提供了一個像訪問數組那樣安全地操作內存的方式。?它非常類似 T[] 或 ArraySegment,它提供安全的訪問內存區域指針的能力。其實我理解它是.NET中操作(void*)指針的抽象封裝,熟悉C/C++開發者應該更明白這意味著什么。

  Span的特點如下:

  • 抽象了所有連續內存空間的類型系統,包括:數組、非托管指針、堆棧指針、fixed或pinned過的托管數據,以及值內部區域的引用;
  • 支持CLR標準對象類型和值類型;
  • 支持泛型;
  • 支持GC,而不像指針需要自己來管理釋放;
  • 例如,我們可以通過一個數組創建一個 Span<T>:

    var arr = new byte[10]; Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>

    由此,利用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 中引入的稱為 ref returns 的 C#語言特性。 索引器使用 “ref T” 返回類型聲明,它提供類似索引到數組的語義,返回對實際存儲位置的引用,而不是返回該位置的副本:

    public struct Span<T> { ref T _reference; int _length; public ref T this[int index] { get {...} }... }public struct ReadOnlySpan<T> { ref T _reference; int _length; public T this[int index] { get {...} }... }

    ref return 索引器帶來的影響可以通過與List<T> 的索引器(它不是 ref return)比較:

    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 的特性,返回的是 ref readonly T 而不是 ref T,這使得它能適用于 不可變的數據類型(immutable data types),例如 String。 ReadOnlySpan<T> 可以在不分配內存和拷貝字符串的情況下,實現對字符串的高效拆分

    string str = "hello, world"; string worldString = str.Substring(startIndex: 7, length: 5); // Allocates ReadOnlySpan<char> worldSpan = str.AsSpan().Slice(start: 7, length: 5); // No allocation Assert.Equal('w', worldSpan[0]); worldSpan[0] = 'a'; // Error CS0200: indexer cannot be assigned to

    Span 還有其他優勢。例如,span 支持 重新解釋 的強制類型轉換。你可以將Span <byte>轉換為Span <int>(其中Span <int> 的第0個索引映射到Span <byte> 的前四個字節)。 這樣,如果讀取字節緩沖,則可以將其傳遞給一個把分組 byte 當作 int 進行操作的方法,該方法可以安全有效地執行。

    Span<T> 是如何實現的?

    開發人員通常不需要了解他們使用的庫是如何實現的。 但是,對于 Span <T>,了解其實現細節是很值得的,從中我們可以推斷出它的性能和使用限制。

    首先,Span <T>是一個包含 ref 和 length 的值類型,大致定義如下:

    public readonly ref struct Span<T> {private readonly ref T _pointer;private readonly int _length;... }

    ref T 字段的概念起初可能很奇怪 —— 實際上,我們不能在C#中甚至在MSIL中聲明 ref T 字段。 但是 Span <T> 實際上是使用 CLR 特殊的內部類型編寫的,它是 JIT 的一個內部函數,JIT 會等效地將該字段生成一個 ref T 字段。

    參考一個更常見的 ref 用法案例:

    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]);

    這段代碼通過引用傳遞數組中的一個槽(slot),這樣(除了優化)你在堆棧上有一個 ref T . Span <T> 中的 ref T 是相同的理念,只是封裝在結構中。 直接或間接包含此類 ref 的類型,被稱為? ref-like 類型,C#7.2 編譯器允許通過在簽名中使用 ref 結構來聲明此類 ref-like 的類型。

    綜上所述,應該清楚兩件事:

  • Span <T> 的定義使得 它的操作 可以與數組一樣高效:索引到 span 中不需要計算來確定指針的起點及其起始偏移量,因為ref 字段本身已經封裝了兩者。 (相比之下,ArraySegment <T>有一個單獨的偏移字段,使索引和傳遞更加低效。)
  • Span <T> 作為 ref-like 類型,由于其ref T字段,也帶來了一些限制。
  • 第二條導致了一些有趣的結果 —— .NET 里有另一個相關類型:Memory <T> 。

    Memory<T> 又是什么鬼?

    Span<T> 中含有一個 ref 字段,ref 字段不僅可以指向對象的開頭,也可以指向對象中間:

    var arr = new byte[100]; Span<byte> interiorRef1 = arr.AsSpan(start: 20); Span<byte> interiorRef2 = new Span<byte>(arr, 20, arr.Length – 20); Span<byte> interiorRef3 =MemoryMarshal.CreateSpan<byte>(arr, ref arr[20], arr.Length – 20);

    這些引用稱為內部指針,跟蹤它們對于 .NET 運行時的 GC 來說是一個相對代價較高的操作。 因此,運行時將這些引用限制在堆棧(stack)上,因為它提供了可能存在的內部指針數量的隱式下限。

    Span<T> 比機器的一個字節要大,這導致一個Span 的讀寫不是原子操作。如果多個線程同時讀寫堆上的 span 的字段,這會存在著線程安全問題。

    因此,Span<T> 的實例只能放在堆棧上,不能放在堆上。因此,不能對 Span<T> 進行裝箱操作(例如,不能對 Span<T>使用已有的反射調用 API,因為他們用到了裝箱)。于是,在類中,不能含有 Span<T> 字段,甚至在 非 ref-like 結構體中也不能有 Span<T> 字段。而且,也不能在可能隱式地成為類的字段的地方使用它,例如把它放在 lambda 中或者在異步方法或迭代器中的局部變量(因為這些局部變量可能會最終成為編譯器生成的狀態機的字段)。也不能把 Span<T> 當做泛型參數來使用,因為該類型參數的實例最終有可能被裝箱或以其他方式被存儲到堆中(目前還沒有 where T : ref struct 限制)。

    這些限制在很多場景下并不重要,特別是對于計算密集型和同步方法。但是異步方法就不一樣了。無論是同步處理操作還是異步處理操作,本文開頭提到的關于數組、數組切片、本機內存等大多數問題都存在。然鵝,如果 Span<T> 無法被存儲在堆中,因此不能跨異步操作進行持久化,那么怎么解決呢?答案就是 Memory<T>。

    Memory<T> 看起來跟 ArraySegment<T> 很像:

    public readonly struct Memory<T> {private readonly object _object;private readonly int _index;private readonly int _length;... }

    你可以從數組創建一個 Memory<T> 然后像 span 一樣切分它。但是它是個 非 ref-like 結構體,因此可以存儲在堆上。于是,你若想做同步處理,你可以用它來創建一個 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) { ... }

    同樣的, Memory<T> 也有一個只讀版本:ReadOnlyMemory<T> ,它的 Span 屬性也返回 ReadOnlySpan<T> 。下表列出了這些類型互相轉換的內建機制:

    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?表1? Span 相關類型之間的 無需內存分配/無需拷貝 的轉換

    FromToMechanism
    ArraySegment<T>Memory<T>隱式轉換, AsMemory() 方法
    ArraySegment<T>ReadOnlyMemory<T>隱式轉換, AsMemory() 方法
    ArraySegment<T>ReadOnlySpan<T>隱式轉換, AsSpan() 方法
    ArraySegment<T>Span<T>隱式轉換, AsSpan() 方法
    ArraySegment<T>T[]Array 屬性
    Memory<T>ArraySegment<T>MemoryMarshal.TryGetArray() 方法
    Memory<T>ReadOnlyMemory<T>隱式轉換, AsMemory() 方法
    Memory<T>Span<T>Span 屬性
    ReadOnlyMemory<T>ArraySegment<T>MemoryMarshal.TryGetArray() 方法
    ReadOnlyMemory<T>ReadOnlySpan<T>Span 屬性
    ReadOnlySpan<T>ref readonly TIndexer get accessor, 一些 marshaling 方法
    Span<T>ReadOnlySpan<T>隱式轉換, AsSpan() 方法
    Span<T>ref TIndexer get accessor, 一些 marshaling 方法
    StringReadOnlyMemory<char>AsMemory() 方法
    StringReadOnlySpan<char>隱式轉換, AsSpan() 方法
    T[]ArraySegment<T>Ctor, 隱式轉換
    T[]Memory<T>Ctor, 隱式轉換, AsMemory() 方法
    T[]ReadOnlyMemory<T>Ctor, 隱式轉換, AsMemory() 方法
    T[]ReadOnlySpan<T>Ctor, 隱式轉換, AsSpan () 方法
    T[]Span<T>Ctor, 隱式轉換, AsSpan() 方法
    void*ReadOnlySpan<T>Ctor
    void*Span<T>Ctor

    ? ??你也許注意到了, Memory<T> 的 _object 字段沒有用 T [] 限定類型,它僅僅是個 object。這表明了 Memory<T> 可以包裝除了數組以外的東西,例如?System.Buffers.OwnedMemory<T>。 OwnedMemory <T> 是一個抽象類,可用于包裝需要嚴格管理生命周期的數據,例如從池中檢索的內存。 這個主題超出了本文范圍,但這就是使用 Memory <T> 來,例如,將指針包裝到本機內存中的機制。ReadOnlyMemory <char> 也可以與字符串一起使用,就像ReadOnlySpan <char> 一樣。

    Span<T> 和 Memory<T> 是如何與 .NET 庫集成的?

    在之前的 Memory <T> 代碼片段中,您會注意到對 Stream.ReadAsync 的調用傳遞了一個 Memory<byte> 參數。但是如今的 .NET 中的Stream.ReadAsync 被定義為接受 byte [] 參數。 這是如何運作的?

    為了支持Span <T>和它的朋友們,在.NET 中添加了數百個新成員和類型。 其中許多是現有的 基于數組和字符串的方法的重載,而有些則是專注于特定處理區域的全新類型。 例如,像 Int32 這樣的所有基本類型 的 Parse() 方法,除了原有的以 string 作為參數的重載以外,現在都具有接受 ReadOnlySpan <char> 作為參數的重載。 想象一下這樣一種情況,你期望解析一個包含兩個以逗號分隔的數字的字符串(例如“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; 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.Sockets,添加了重載以使 {ReadOnly} Span <T>和 {ReadOnly} Memory <T> 變得簡單而高效。 其中一些甚至帶來額外的好處。 例如,Stream現在有這個方法:

    public virtual ValueTask<int> ReadAsync( Memory<byte> destination,CancellationToken cancellationToken = default) { ... }

    注意到,與接受 byte [] 并返回 Task <int> 的現有 ReadAsync 方法不同,此重載不僅接受 Memory <byte> ,而且還返回 ValueTask <int> 而不是 Task<int>。 ValueTask <T> 是一個結構,它可以避免以下兩種情況的內存分配:(1)異步方法頻繁進行同步返回;(2)難以緩存所有公共返回值。 例如,運行時可以將完成的Task <bool> 緩存為 true 或者 false,但是它不能為 Task <int> 的所有可能結果值緩存40億個 int 對象。

    由于在?Stream 的實現中,我們經常以同步的方式調用?ReadAsync 來緩沖數據,所以這個新的 ReadAsync 重載返回一個ValueTask <int>。 這意味著同步完成的異步流讀取操作可以不必再分配內存。 ValueTask <T>也用于其他新的重載,例如 Socket.ReceiveAsync,Socket.SendAsync,WebSocket.ReceiveAsync 和 TextReader.ReadAsync 的重載。

    此外,還有一些地方,Span <T> 允許框架包含過去引起內存安全問題的方法。 考慮一種情況:你希望創建一個由隨機生成的 char 組成的字符串,例如某種 ID。 你可能需要分配一個 char 數組,如下所示:

    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);

    你可以使用 堆棧分配(stack-allocation),甚至利用 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>允許這樣做。 除了string的新構造函數之外,string現在還有一個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<char> 相當于是一個指針,指向了堆中的字符串)

    現在,我們不僅避免了內存分配,而且實現了直接將內容寫到字符串在堆上的內存。這意味著我們避免了復制,因此可以不受堆棧大小的限制。

    除了擴展了框架中一些核心類型的成員變量之外,微軟還在持續開發新的 .NET 類型,以便使用 Span 高效處理某些特定的場景。 例如,對于編寫高性能微服務和大量文本處理的 Web 站點的開發人員來說,如果在使用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(); }

    所有這些功能不僅僅是為了給公眾使用,相反,Framework 本身能夠利用這些 基于Span <T> 和 Memory <T> 的新方法來提升性能。 跨.NET Core 的調用站點已切換到使用新的 ReadAsync 重載以避免不必要的分配。 以往需要分配子字符串才能完成的解析功能,現在都利用了無分配的方式進行解析。 甚至像 Rfc2898DeriveBytes 這樣的小眾類型都進行了改進,利用System.Security.Cryptography.HashAlgorithm 上新的基于 Span <byte> 的 TryComputeHash() 方法節省了大量的內存空間(每次迭代算法的字節數組, 這可能會重復數千次),提高了吞吐量。

    這并不止于核心 .NET 庫的層次,它也延伸到堆棧中。 ASP.NET Core 現在嚴重依賴于 Span,例如,在它們之上編寫了 Kestrel 服務器的HTTP 解析器。 將來,Span 可能會暴露在較低級別的 ASP.NET Core 的公共 API 之外,例如在其中間件管道中。

    .NET 運行時有何變化?

    .NET 運行時確保安全性的方法之一是確保數組索引不超出數組的長度,這種做法稱為邊界檢查。 例如這個方法:

    [MethodImpl(MethodImplOptions.NoInlining)] static int Return4th(int[] data) => data[3];

    在 X64 平臺上,生成的程序集如下:

    sub rsp, 40cmp dword ptr [rcx+8], 3jbe SHORT G_M22714_IG04mov eax, dword ptr [rcx+28]add rsp, 40ret G_M22714_IG04:call CORINFO_HELP_RNGCHKFAILint3

    其中的 cmp 指令將數據數組的長度與索引3進行比較,隨后的 jbe 指令跳轉到范圍檢查失敗例程,如果3超出范圍(對于要拋出的異常)。 JIT 需要生成代碼以確保此類訪問不會超出數組的范圍,但這并不意味著每個單獨的數組訪問都需要綁定檢查。 考慮這個Sum方法:

    static int Sum(int[] data) {int sum = 0;for (int i = 0; i < data.Length; i++) sum += data[i];return sum; }

    這里 JIT 需要生成代碼,以確保對數據 [i] 的訪問不會超出數組的范圍,但是因為JIT可以從循環的結構告訴我將始終在范圍內(循環迭代) 通過從開始到結束的每個元素,JIT 可以優化數組上的邊界檢查。 因此,為循環生成的匯編代碼如下所示:

    G_M33811_IG03:movsxd r9, edxadd eax, dword ptr [rcx+4*r9+16]inc edxcmp r8d, edxjg SHORT G_M33811_IG03

    cmp 指令依然存在,但只是將 i 的值(存儲在edx寄存器中)與數組的長度(存儲在r8d寄存器中)進行比較; 沒有額外的邊界檢查。

    運行時Runtime 將類似的優化應用于 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, r8dadd ecx, dword ptr [rax+4*r9]inc r8dcmp r8d, edxjl SHORT G_M33812_IG03

    匯編代碼非常相似,部分原因是消除了邊界檢查。 但同樣重要的是 JIT 將 span 索引器識別為內部的,這意味著JIT為索引器生成特殊代碼,而不是將其實際的IL代碼轉換為匯編。

    所有這些都是為了說明運行時就像 Array 一樣 可以為 Span 做優化,從而使 Span 成為訪問數據的有效機制。 更多詳細信息可在博客?bit.ly/2zywvyI 中找到。

    C# 語言及其編譯器有啥變化?

    我已經提到了 C#語言和編譯器新增的功能,這些功能使得 Span <T> 成為 .NET 中的上等公民。 C#7.2 的幾個特性與 Span 相關(事實上,使用 Span <T> 需要C#7.2 編譯器)。 我們來看看三個這樣的功能。

    (1)引用結構(Ref Struct)

    如前所述,Span <T> 是一種 ref-like 類型,它在 C# 7.2 中作為 ref struct 公布。 通過在 struct 之前放置 ref關鍵字,您可以告訴 C#編譯器允許您使用其他 ref struct 類型(如Span <T>)作為字段,并且這種約束也會傳遞到將要分配的類型中。?例如,如果你想為Span <T> 編寫一個 struct Enumerator,那么 Enumerator 需要存儲Span <T>,因此,它本身需要是一個ref 結構,如下所示:

    public ref struct Enumerator {private readonly Span<char> _span;private int _index;... }

    (2)Span 的 Stackalloc 初始化(Stackalloc initialization of spans)。?

    在以前的C#版本中,stackalloc 的結果只能存儲在指針局部變量中。 從C#7.2開始,stackalloc 現在可以用作表達式的一部分并且可以指向一個 Span,并且可以在不使用 unsafe 關鍵字的情況下完成。 因此,我們不必再這樣寫:

    Span<byte> bytes; unsafe {byte* tmp = stackalloc byte[length];bytes = new Span<byte>(tmp, length); }

    我們可以這么寫:

    Span<byte> bytes = stackalloc byte[length];

    在需要一些臨時空間來執行操作,但希望避免分配相對較小的堆內存的情況下,這也非常有用。 在以前,有兩種實現方式:

    • 編寫兩個完全不同的代碼路徑,一個分配棧的內存并進行相關操作,另一個基于堆內存進行操作。
    • 固定與分配的托管內存,然后委托給同樣是基于棧的、使用 unsafe 指針代碼的內存的實現。

    現在,使用安全的代碼和盡量少的折騰,同樣的事情可以在沒有代碼冗余的情況下完成:

    Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length]; ... // Code that operates on the Span<byte>

    (3)Span 使用驗證(Span usage validation)

    因為 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#編譯器使用 ref 結構檢測到這種無效用法,并且編譯失敗并出現錯誤:

    Error CS8352:在此上下文中不能使用本地 “chars”,因為它可能會在其聲明范圍之外暴露引用的變量

    接下來呢?

    這里討論的類型,方法,運行時優化和其他元素有望包含在.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。

    總結

    以上是生活随笔為你收集整理的Span<T> —— .NET Core 高效运行的新基石的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。