`Span<T>` 是 .NET Core 2.1 引入的一個新類型,它提供任意內存的連續區域的類型安全和內存安全表示形式。`Span<T>` 可以與任意的值類型或引用類型進行關聯,包括原始內存指針、數組、堆上對象等。通過 `Span<T>`,我們可以對這些數據結構進行高效的讀取和寫入操作,而無需進行拷貝或者分配額外的內存。
在 .NET 中,許多常見的數據類型,如 `string`、`array` 等,都是引用類型,它們本身并不包含實際的數據,而是在堆上分配了一塊內存來存儲數據,然后將其地址傳遞給變量。這種設計在很多情況下非常方便,但也會帶來一些性能上的問題,比如頻繁的內存分配和釋放、GC 壓力等。`Span<T>` 的出現為解決這些問題提供了一種新的方式。
使用 `Span<T>`,我們可以盡可能地避免進行內存分配和復制,從而提高代碼的運行效率。同時,由于 `Span<T>` 只是一個“視圖”,它并不會改變原始數據的內容或生命周期,因此也非常安全可靠。在 .NET Core 中,許多常見的 API(如網絡、IO、序列化等)都已經開始支持 `Span<T>`,這為我們編寫高性能、低延遲的代碼提供了更多的可能性。
通常不需要了解他們正在使用的庫是如何實現的。但是,就 Span<T> 而言,至少對其背后的細節有一個基本的了解是值得的,因為這些細節暗示了其性能和使用限制。
首先,Span<T> 是一個包含 ref 和長度的值類型,定義大致如下:
public readonly ref struct Span<T>{ private readonly ref T _pointer; private readonly int _length; ...}
引用 T 字段的概念一開始可能很奇怪,事實上,實際上無法在 C# 甚至 MSIL 中聲明引用 T 字段。但 Span<T> 實際上是為在運行時中使用一種特殊的內部類型而編寫的,該類型被視為實時 (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]);
此代碼通過引用傳遞數組中的插槽,這樣(撇開優化不談)堆棧上有一個 ref T。Span<T> 中的 ref T 是相同的想法,只是封裝在一個結構中。直接或間接包含此類 ref 的類型稱為類似 ref 的類型,C# 7.2 編譯器允許通過在簽名中使用 ref 結構來聲明此類類似 ref 的類型。
從這個簡短的描述中,應該清楚兩件事:
Span<T> 是在堆棧而不是托管堆上分配的 ref 結構 。 Ref 結構類型有許多限制,以確保它們不能提升到托管堆,包括不能裝箱、不能分配給 類型的Objectdynamic變量或任何接口類型,它們不能是引用類型中的字段,也不能跨 await 和 yield 邊界使用。 此外,對和 兩個NotSupportedException方法的 Equals(Object)GetHashCode調用會引發 。
因為它是僅堆棧類型, Span<T> 不適用于許多需要存儲對堆上的緩沖區的引用的方案。 例如,進行異步方法調用的例程也是如此。 對于此類方案,可以使用互補 System.Memory<T> 和 System.ReadOnlyMemory<T> 類型。
Span<T> 可以看作是一個指向連續內存塊的引用,它可以用于訪問數組、堆棧、堆等數據結構中的連續元素。Span<T> 對象本身不會分配或釋放任何內存,因此它非常適用于內存密集型的應用場景,例如網絡編程、高性能計算等。
以下是一些 Span<T> 的常見應用場景:
在 .NET 中,可以通過以下幾種方式來創建 Span<T> 對象:
unsafe{ int[] array = { 1, 2, 3, 4 }; fixed (int* ptr = array) { Span<int> span = new Span<int>(ptr, array.Length); // 對 span 進行操作 }}
在這個例子中,我們首先通過 fixed 關鍵字將 array 數組的地址固定下來,然后使用 new Span<int>(ptr, array.Length) 構造函數創建一個 Span<int> 對象,該對象引用了整個 array 數組。
int[] array = { 1, 2, 3, 4 };Span<int> span = new Span<int>(array);// 對 span 進行操作
在這個例子中,我們直接使用 array 數組創建了一個 Span<int> 對象,該對象引用了整個數組。
int[] array = { 1, 2, 3, 4 };Span<int> span = new Span<int>(array, 1, 2);// 對 span 進行操作
在這個例子中,我們使用 new Span<int>(array, 1, 2) 構造函數創建了一個 Span<int> 對象,該對象引用了 array 數組的第二個元素和第三個元素。
string str = "hello world";Span<char> span = str.AsSpan();// 對 span 進行操作
在這個例子中,我們使用 AsSpan 方法將一個字符串轉換為 Span<char> 對象,該對象引用了字符串的所有字符。
除了上述方式外,還可以使用 Memory<T> 或者 ReadOnlyMemory<T> 類型來創建 Span<T> 對象。Memory<T> 表示一個可變的內存區域,而 ReadOnlyMemory<T> 表示一個不可變的內存區域,它們都可以用來創建 Span<T> 對象。例如:
int[] array = { 1, 2, 3, 4 };Memory<int> memory = new Memory<int>(array);Span<int> span = memory.Span;// 對 span 進行操作
在這個例子中,我們首先使用 new Memory<int>(array) 構造函數創建了一個 Memory<int> 對象,然后使用 Span 屬性獲取了其對應的 Span<int> 對象。
使用 Span<T> 可以避免數據拷貝和內存分配的開銷,從而提高網絡編程的性能和效率。一般情況下,網絡數據包的二進制數據往往是連續存儲在內存中的,Span<T> 可以直接引用該內存塊,而不需要進行額外的拷貝操作。
使用 Span<T> 解析網絡數據包的一般步驟:
`Span<T>` 在網絡編程中可以提供高性能的內存訪問和數據處理,從而提升網絡應用程序的效率。下面是幾個使用 `Span<T>` 進行網絡編程的常見場景:
數據接收和解析:使用 `Socket` 接收到的字節數據可以直接轉換為 `Span<byte>`,避免了額外的內存拷貝操作。然后,可以使用 `Span<T>` 提供的方法對數據進行解析,例如檢查數據包的長度、提取字段值等。
byte[] buffer = new byte[1024];int bytesRead = socket.Receive(buffer); // 從 Socket 接收數據Span<byte> data = buffer.AsSpan(0, bytesRead);// 解析數據包...`
數據發送:使用 `Span<T>` 可以直接將數據發送到網絡中,而無需將數據復制到新的緩沖區中。這樣可以避免內存拷貝的開銷,提高發送數據的效率。
byte[] data = GetPacketData(); // 獲取待發送的數據socket.Send(data.AsSpan()); // 直接發送數據
數據處理和轉換:在網絡通信中,涉及到各種數據格式的轉換和處理操作。使用 `Span<T>` 可以方便地對字節數據進行解析、轉換和修改。
byte[] receivedData = ReceiveDataFromSocket(); // 從 Socket 接收數據// 將接收到的數據轉換為字符串string message = Encoding.UTF8.GetString(receivedData.AsSpan());// 修改數據并發送回去receivedData.AsSpan().Reverse(); // 反轉字節順序SendDataToSocket(receivedData);`
緩沖區池化:在高并發的網絡應用程序中,使用緩沖區池化技術可以避免頻繁的內存分配和釋放操作,提高性能。`Span<T>` 可以與緩沖區池化技術相結合,共享和重用緩沖區,減少內存開銷。
ArrayPool<byte> bufferPool = ArrayPool<byte>.Shared;byte[] buffer = bufferPool.Rent(1024); // 從緩沖區池中租借一個緩沖區int bytesRead = socket.Receive(buffer); // 從 Socket 接收數據Span<byte> data = buffer.AsSpan(0, bytesRead);// 處理接收到的數據...bufferPool.Return(buffer); // 將緩沖區歸還給緩沖區池`
通過合理地利用 `Span<T>` 的特性和方法,我們可以在網絡編程中實現高效的數據處理和傳輸,提升網絡應用程序的性能和可伸縮性。但需要注意的是,使用 `Span<T>` 時要小心懸掛指針和內存安全問題,確保操作的內存是有效的并且不會被修改。
使用 Socket 和 Span<T> 進行網絡數據包解析是一種高效、低內存消耗的方式。下面是一個簡單的示例,演示如何使用這兩個類型進行網絡數據包解析:
// 假設已經建立了一個 TCP 連接,并且從 Socket 接收到了一段字節數據byte[] buffer = new byte[1024]; // 接收數據的緩沖區int bytesRead = socket.Receive(buffer); // 從 Socket 接收數據Span<byte> data = buffer.AsSpan(0, bytesRead); // 將接收到的字節數據轉換為 Span<byte>// 解析數據包while (data.Length > 0){ // 檢查數據包的長度是否足夠 if (data.Length < sizeof(int)) { // 數據不完整,等待下一次接收 break; } // 讀取數據包的長度 int packetLength = BitConverter.ToInt32(data); // 檢查數據包是否完整 if (data.Length < packetLength + sizeof(int)) { // 數據不完整,等待下一次接收 break; } // 提取數據包內容 Span<byte> packetData = data.Slice(sizeof(int), packetLength); // 處理數據包 ProcessPacket(packetData); // 調整指針,繼續處理剩余數據 data = data.Slice(sizeof(int) + packetLength);}
在上面的示例中,我們首先從 Socket 接收數據,并將接收到的字節數據存儲在一個字節數組中。然后,我們使用 AsSpan 方法將字節數組轉換為 Span<byte>,以便進行高效的數據包解析。
接下來,我們使用一個循環來處理數據包。在每次循環中,我們首先檢查是否有足夠的數據來讀取數據包的長度。如果數據不完整,我們等待下一次接收。如果有足夠的數據,我們讀取數據包的長度,并檢查是否有足夠的數據來完整解析數據包。如果數據不完整,我們等待下一次接收。
一旦我們獲得了完整的數據包,我們可以使用 Slice 方法提取數據包的內容,并進行相應的處理操作(例如解析數據、驗證數據、處理數據等)。
通過使用 Span<T> 和適當的循環邏輯,我們可以高效地解析網絡數據包,減少內存拷貝和數據處理的開銷,從而提高網絡應用程序的性能和效率。
本文鏈接:http://www.www897cc.com/showinfo-26-88728-0.htmlSpan 在網絡編程中可以提供高性能的內存訪問和數據處理能力
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: Hive SQL底層執行過程詳細剖析