在.net开发中,内存管理一直是影响性能的关键因素。传统的字符串处理、数组操作等往往伴随着大量的内存分配和复制操作,这些不必要的开销在高性能场景下尤为明显。
为了解决这个问题,.net core 2.1引入了span和memory这两个强大的类型,它们能够:
- 显著减少内存分配
- 提升数据操作性能
- 安全地访问连续内存区域
- 支持多种内存来源的统一操作
span:栈上分配的高性能利器
span的本质
span是一个栈分配的结构体(值类型),它提供了一种不需要额外内存分配就能操作连续内存区域的方法。
int[] numbers = { 1, 2, 3, 4, 5 };
span<int> span = numbers;
span[0] = 10;
console.writeline(numbers[0]);
注意:数组堆上分配的引用类型,与span还是有区别的,span无gc压力。
span与字符串处理
传统的字符串处理方法如substring()会创建新的字符串实例,而使用span可以避免这种额外的内存分配:
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()}");
}
}
使用stackalloc与span
span可以直接与栈上分配的内存一起使用,避免堆分配的开销:
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();
}
}
}
span的关键特性
- 零内存分配操作数据时不创建额外的内存对象
- 类型安全提供类型检查,避免类型转换错误
- 可用于多种内存来源数组、固定大小缓冲区、栈分配内存、非托管内存等
- 性能优势适用于高性能计算和数据处理场景
- 限制只能在同步方法中使用,不能作为类的字段
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()
{
// 创建一个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())}");
}
}
}
memory的关键特性
- 异步友好可以在异步方法中使用
- 不绑定执行上下文可以在方法之间传递
- 可作为类字段可以存储在类中长期使用
- 性能略低相比span有轻微的性能开销
- 更灵活可用于更多场景
span与memory的对比选择
特性 | span<t> | memory<t> |
分配位置 | 栈 | 堆 |
异步支持 | 不支持 | 支持 |
性能表现 | 更高 | 稍低 |
适用场景 | 同步高性能操作 | 异步操作、跨方法传递 |
可否作为字段 | 不可以 | 可以 |
生命周期 | 方法范围内 | 可长期存在 |
实战应用场景
高性能字符串解析
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()}'");
}
}
}
二进制数据处理
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)
{
thrownew argumentexception("数据格式不正确");
}
// 检查头部标识"data"
readonlyspan<byte> header = data.slice(0, 4);
if (!(header[0] == 'd' && header[1] == 'a' && header[2] == 't' && header[3] == 'a'))
{
thrownew argumentexception("无效的文件头");
}
// 读取数据长度 (小端字节序)
int datalength = binaryprimitives.readint32littleendian(data.slice(4, 4));
// 确保数据完整
if (data.length < 8 + datalength)
{
thrownew 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的建议
- 不要尝试将span作为字段存储
- 不要将span用于异步方法
- 避免将span装箱(boxing)
- 小心span的生命周期管理,特别是使用stackalloc时
- 使用readonlyspan表示不需要修改的数据
memory的最佳实践
- 优先考虑readonlymemory而非memory(当不需要修改数据时)
- 在异步操作中使用memory替代数组
- 在需要长期保留引用时使用memory而非span
- 需要操作时才调用.span属性,不要过早转换
兼容性与平台支持
span和memory支持情况:
- .net core 2.1及更高版本
- .net standard 2.1
- .net 5/6/7/8及以后版本
- 不完全支持.net framework,但可通过system.memory nuget包获得部分支持
总结
span和memory是c#中处理高性能内存操作的强大工具,它们能够:
- 减少内存分配和gc压力通过避免不必要的内存分配和复制
- 提高性能特别是在处理大量数据和频繁字符串操作时
- 保持类型安全避免了使用unsafe代码和指针操作的风险
- 简化代码提供了直观的api来处理连续内存区域
在实际开发中,记住这些简单的选择规则:
- 对于同步方法中的高性能操作,选择span
- 对于异步方法或需要跨方法传递的场景,选择memory
掌握这两个强大的工具,将帮助你编写更高效、更可靠的c#代码,特别是在处理大数据量、高性能要求的应用场景中。
发表评论