C#中的高性能內(nèi)存操作的利器:Span<T>和Memory<T>
在.NET開發(fā)中,內(nèi)存管理一直是影響性能的關鍵因素。傳統(tǒng)的字符串處理、數(shù)組操作等往往伴隨著大量的內(nèi)存分配和復制操作,這些不必要的開銷在高性能場景下尤為明顯。
為了解決這個問題,.NET Core 2.1引入了Span和Memory這兩個強大的類型,它們能夠:
- 顯著減少內(nèi)存分配
- 提升數(shù)據(jù)操作性能
- 安全地訪問連續(xù)內(nèi)存區(qū)域
- 支持多種內(nèi)存來源的統(tǒng)一操作
Span:棧上分配的高性能利器
Span的本質(zhì)
Span是一個棧分配的結(jié)構(gòu)體(值類型),它提供了一種不需要額外內(nèi)存分配就能操作連續(xù)內(nèi)存區(qū)域的方法。
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers;
span[0] = 10;
Console.WriteLine(numbers[0]);
注意:數(shù)組堆上分配的引用類型,與Span還是有區(qū)別的,Span無GC壓力。
Span與字符串處理
傳統(tǒng)的字符串處理方法如Substring()會創(chuàng)建新的字符串實例,而使用Span可以避免這種額外的內(nèi)存分配:
using System;
class Program
{
static void Main()
{
string orderData = "ORD-12345-AB: 已發(fā)貨";
// 傳統(tǒng)方式 - 創(chuàng)建新的字符串對象
string orderId1 = orderData.Substring(0, 11); // 分配新內(nèi)存
string status1 = orderData.Substring(13); // 再次分配新內(nèi)存
// 使用Span<T> - 不創(chuàng)建新的字符串對象
ReadOnlySpan<char> dataSpan = orderData.AsSpan();
ReadOnlySpan<char> orderId2 = dataSpan.Slice(0, 11); // 不分配新內(nèi)存
ReadOnlySpan<char> status2 = dataSpan.Slice(13); // 不分配新內(nèi)存
// 必要時才將Span轉(zhuǎn)換為string
Console.WriteLine($"訂單號: {orderId2.ToString()}");
Console.WriteLine($"狀態(tài): {status2.ToString()}");
}
}
使用stackalloc與Span
Span可以直接與棧上分配的內(nèi)存一起使用,避免堆分配的開銷:
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AppSpanMemory
{
internal class Program
{
static unsafe void Main()
{
Span<int> stackNums = stackalloc int[100];
for (int i = 0; i < stackNums.Length; i++)
{
stackNums[i] = i * 10;
}
// 獲取Span起始位置的指針
void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference(stackNums));
Console.WriteLine($"Span內(nèi)存地址: 0x{(ulong)ptr:X}");
// 打印前10個元素
var firstTen = stackNums.Slice(0, 10);
foreach (var n in firstTen)
{
Console.Write($"{n} ");
}
Console.ReadKey();
}
}
}
Span的關鍵特性
- 零內(nèi)存分配操作數(shù)據(jù)時不創(chuàng)建額外的內(nèi)存對象
- 類型安全提供類型檢查,避免類型轉(zhuǎn)換錯誤
- 可用于多種內(nèi)存來源數(shù)組、固定大小緩沖區(qū)、棧分配內(nèi)存、非托管內(nèi)存等
- 性能優(yōu)勢適用于高性能計算和數(shù)據(jù)處理場景
- 限制只能在同步方法中使用,不能作為類的字段
Memory:異步操作的理想選擇
Memory的定位
Memory是Span的堆分配版本,主要用于支持異步操作場景。
// Memory<T>的基本使用
Memory<int> memory = new int[] { 1, 2, 3, 4, 5 };
Span<int> spanFromMemory = memory.Span; // 從Memory獲取Span視圖
spanFromMemory[0] = 20;
Console.WriteLine(memory.Span[0]);
Memory與異步文件操作
Memory在處理異步I/O操作時特別有用:
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AppSpanMemory
{
internal class Program
{
static async Task Main()
{
// 創(chuàng)建一個4KB的緩沖區(qū)
byte[] buffer = new byte[4096];
Memory<byte> memoryBuffer = buffer;
using FileStream fileStream = new FileStream("bigdata.dat", FileMode.Open, FileAccess.Read);
int bytesRead = await fileStream.ReadAsync(memoryBuffer);
if (bytesRead > 0)
{
Memory<byte> actualData = memoryBuffer.Slice(0, bytesRead);
ProcessData(actualData.Span);
}
Console.WriteLine($"讀取了 {bytesRead} 字節(jié)的數(shù)據(jù)");
}
static void ProcessData(Span<byte> data)
{
Console.WriteLine($"前10個字節(jié): {BitConverter.ToString(data.Slice(0, Math.Min(10, data.Length)).ToArray())}");
}
}
}
Memory的關鍵特性
- 異步友好可以在異步方法中使用
- 不綁定執(zhí)行上下文可以在方法之間傳遞
- 可作為類字段可以存儲在類中長期使用
- 性能略低相比Span有輕微的性能開銷
- 更靈活可用于更多場景
Span與Memory的對比選擇
特性 | Span<T> | Memory<T> |
分配位置 | 棧 | 堆 |
異步支持 | 不支持 | 支持 |
性能表現(xiàn) | 更高 | 稍低 |
適用場景 | 同步高性能操作 | 異步操作、跨方法傳遞 |
可否作為字段 | 不可以 | 可以 |
生命周期 | 方法范圍內(nèi) | 可長期存在 |
實戰(zhàn)應用場景
高性能字符串解析
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AppSpanMemory
{
internal class Program
{
static async Task Main()
{
string csvLine = "張三,30,北京市海淀區(qū),軟件工程師";
ParseCsvLine(csvLine.AsSpan());
}
public static void ParseCsvLine(ReadOnlySpan<char> line)
{
int start = 0;
int fieldIndex = 0;
for (int i = 0; i < line.Length; i++)
{
if (line[i] == ',')
{
// 不創(chuàng)建新字符串
ReadOnlySpan<char> field = line.Slice(start, i - start);
ProcessField(fieldIndex, field);
start = i + 1;
fieldIndex++;
}
}
// 處理最后一個字段
if (start < line.Length)
{
ReadOnlySpan<char> lastField = line.Slice(start);
ProcessField(fieldIndex, lastField);
}
}
private static void ProcessField(int index, ReadOnlySpan<char> field)
{
Console.WriteLine($"字段 {index}: '{field.ToString()}'");
}
}
}
二進制數(shù)據(jù)處理
using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace AppSpanMemory
{
internal class Program
{
static async Task Main()
{
string csvLine = "張三,30,北京市海淀區(qū),軟件工程師";
byte[] payloadBytes = Encoding.UTF8.GetBytes(csvLine);
// 頭部4字節(jié) + 數(shù)據(jù)長度4字節(jié) + 數(shù)據(jù)體
byte[] fileData = new byte[4 + 4 + payloadBytes.Length];
// 寫入頭部標識 "DATA"
fileData[0] = (byte)'D';
fileData[1] = (byte)'A';
fileData[2] = (byte)'T';
fileData[3] = (byte)'A';
// 寫入數(shù)據(jù)長度(小端)
BinaryPrimitives.WriteInt32LittleEndian(fileData.AsSpan(4, 4), payloadBytes.Length);
// 寫入數(shù)據(jù)體
payloadBytes.CopyTo(fileData.AsSpan(8));
// 傳入文件字節(jié)數(shù)據(jù)的只讀切片
ProcessBinaryFile(fileData);
}
public static void ProcessBinaryFile(ReadOnlySpan<byte> data)
{
// [4字節(jié)頭部標識][4字節(jié)數(shù)據(jù)長度][實際數(shù)據(jù)]
if (data.Length < 8)
{
thrownew ArgumentException("數(shù)據(jù)格式不正確");
}
// 檢查頭部標識"DATA"
ReadOnlySpan<byte> header = data.Slice(0, 4);
if (!(header[0] == 'D' && header[1] == 'A' && header[2] == 'T' && header[3] == 'A'))
{
thrownew ArgumentException("無效的文件頭");
}
// 讀取數(shù)據(jù)長度 (小端字節(jié)序)
int dataLength = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
// 確保數(shù)據(jù)完整
if (data.Length < 8 + dataLength)
{
thrownew ArgumentException("數(shù)據(jù)不完整");
}
// 獲取實際數(shù)據(jù)部分
ReadOnlySpan<byte> payload = data.Slice(8, dataLength);
Console.WriteLine($"有效載荷大小: {payload.Length} 字節(jié)");
Console.WriteLine($"前10個字節(jié): {BitConverter.ToString(payload.Slice(0, Math.Min(10, payload.Length)).ToArray())}");
}
}
}
使用注意事項
安全使用Span的建議
- 不要嘗試將Span作為字段存儲
- 不要將Span用于異步方法
- 避免將Span裝箱(boxing)
- 小心Span的生命周期管理,特別是使用stackalloc時
- 使用ReadOnlySpan表示不需要修改的數(shù)據(jù)
Memory的最佳實踐
- 優(yōu)先考慮ReadOnlyMemory而非Memory(當不需要修改數(shù)據(jù)時)
- 在異步操作中使用Memory替代數(shù)組
- 在需要長期保留引用時使用Memory而非Span
- 需要操作時才調(diào)用.Span屬性,不要過早轉(zhuǎn)換
兼容性與平臺支持
Span和Memory支持情況:
- .NET Core 2.1及更高版本
- .NET Standard 2.1
- .NET 5/6/7/8及以后版本
- 不完全支持.NET Framework,但可通過System.Memory NuGet包獲得部分支持
總結(jié)
Span和Memory是C#中處理高性能內(nèi)存操作的強大工具,它們能夠:
- 減少內(nèi)存分配和GC壓力通過避免不必要的內(nèi)存分配和復制
- 提高性能特別是在處理大量數(shù)據(jù)和頻繁字符串操作時
- 保持類型安全避免了使用unsafe代碼和指針操作的風險
- 簡化代碼提供了直觀的API來處理連續(xù)內(nèi)存區(qū)域
在實際開發(fā)中,記住這些簡單的選擇規(guī)則:
- 對于同步方法中的高性能操作,選擇Span
- 對于異步方法或需要跨方法傳遞的場景,選擇Memory
掌握這兩個強大的工具,將幫助你編寫更高效、更可靠的C#代碼,特別是在處理大數(shù)據(jù)量、高性能要求的應用場景中。
相關文章
c#?使用線程對串口serialPort進行收發(fā)數(shù)據(jù)(四種)
本文主要介紹了c#?使用線程對串口serialPort進行收發(fā)數(shù)據(jù),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07
DataGridView實現(xiàn)點擊列頭升序和降序排序
這篇文章介紹了DataGridView實現(xiàn)點擊列頭升序和降序排序的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02
C#實現(xiàn)微信跳一跳小游戲的自動跳躍助手開發(fā)實戰(zhàn)
前段時間微信更新了新版本后,帶來的一款H5小游戲“跳一跳”在各朋友圈里又火了起來,類似以前的“打飛機”游戲,這游戲玩法簡單,但加上了積分排名功能后,卻成了“裝逼”的地方,于是很多人花錢花時間的刷積分搶排名2018-01-01

