在.NET开发中,内存管理一直是影响性能的关键因素。传统的字符串处理、数组操作等往往伴随着大量的内存分配和复制操作,这些不必要的开销在高性能场景下尤为明显。
为了解决这个问题,.NET Core 2.1引入了Span和Memory这两个强大的类型,它们能够:
Span是一个栈分配的结构体(值类型),它提供了一种不需要额外内存分配就能操作连续内存区域的方法。
C#
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers;
span[0] = 10;
Console.WriteLine(numbers[0]);
注意:数组堆上分配的引用类型,与Span还是有区别的,Span无GC压力。
传统的字符串处理方法如Substring()
会创建新的字符串实例,而使用Span可以避免这种额外的内存分配:
C#using System;
class Program
{
static void Main()
{
string orderData = "ORD-12345-AB: 已发货";
// 传统方式 - 创建新的字符串对象
string orderId1 = orderData.Substring(0, 11); // 分配新内存
string status1 = orderData.Substring(13); // 再次分配新内存
// 使用Span<T> - 不创建新的字符串对象
ReadOnlySpan<char> dataSpan = orderData.AsSpan();
ReadOnlySpan<char> orderId2 = dataSpan.Slice(0, 11); // 不分配新内存
ReadOnlySpan<char> status2 = dataSpan.Slice(13); // 不分配新内存
// 必要时才将Span转换为string
Console.WriteLine($"订单号: {orderId2.ToString()}");
Console.WriteLine($"状态: {status2.ToString()}");
}
}
Span可以直接与栈上分配的内存一起使用,避免堆分配的开销:
C#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内存地址: 0x{(ulong)ptr:X}");
// 打印前10个元素
var firstTen = stackNums.Slice(0, 10);
foreach (var n in firstTen)
{
Console.Write($"{n} ");
}
Console.ReadKey();
}
}
}
Memory是Span的堆分配版本,主要用于支持异步操作场景。
C#// 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在处理异步I/O操作时特别有用:
C#using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AppSpanMemory
{
internal class Program
{
static async Task Main()
{
// 创建一个4KB的缓冲区
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} 字节的数据");
}
static void ProcessData(Span<byte> data)
{
Console.WriteLine($"前10个字节: {BitConverter.ToString(data.Slice(0, Math.Min(10, data.Length)).ToArray())}");
}
}
}
特性 | Span | Memory |
---|---|---|
分配位置 | 栈 | 堆 |
异步支持 | 不支持 | 支持 |
性能表现 | 更高 | 稍低 |
适用场景 | 同步高性能操作 | 异步操作、跨方法传递 |
可否作为字段 | 不可以 | 可以 |
生命周期 | 方法范围内 | 可长期存在 |
C#using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AppSpanMemory
{
internal class Program
{
static async Task Main()
{
string csvLine = "张三,30,北京市海淀区,软件工程师";
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] == ',')
{
// 不创建新字符串
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()}'");
}
}
}
C#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,北京市海淀区,软件工程师";
byte[] payloadBytes = Encoding.UTF8.GetBytes(csvLine);
// 头部4字节 + 数据长度4字节 + 数据体
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';
// 写入数据长度(小端)
BinaryPrimitives.WriteInt32LittleEndian(fileData.AsSpan(4, 4), payloadBytes.Length);
// 写入数据体
payloadBytes.CopyTo(fileData.AsSpan(8));
// 传入文件字节数据的只读切片
ProcessBinaryFile(fileData);
}
public static void ProcessBinaryFile(ReadOnlySpan<byte> data)
{
// [4字节头部标识][4字节数据长度][实际数据]
if (data.Length < 8)
{
throw new ArgumentException("数据格式不正确");
}
// 检查头部标识"DATA"
ReadOnlySpan<byte> header = data.Slice(0, 4);
if (!(header[0] == 'D' && header[1] == 'A' && header[2] == 'T' && header[3] == 'A'))
{
throw new ArgumentException("无效的文件头");
}
// 读取数据长度 (小端字节序)
int dataLength = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
// 确保数据完整
if (data.Length < 8 + dataLength)
{
throw new ArgumentException("数据不完整");
}
// 获取实际数据部分
ReadOnlySpan<byte> payload = data.Slice(8, dataLength);
Console.WriteLine($"有效载荷大小: {payload.Length} 字节");
Console.WriteLine($"前10个字节: {BitConverter.ToString(payload.Slice(0, Math.Min(10, payload.Length)).ToArray())}");
}
}
}
Span和Memory支持情况:
Span和Memory是C#中处理高性能内存操作的强大工具,它们能够:
在实际开发中,记住这些简单的选择规则:
掌握这两个强大的工具,将帮助你编写更高效、更可靠的C#代码,特别是在处理大数据量、高性能要求的应用场景中。
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!