在现代 .NET 高性能开发中,Span<T> 与 Memory<T> 已成为绕不开的核心工具。它们的目标非常明确:减少内存分配、避免数据复制、降低 GC 压力。尤其在处理字符串、二进制数据、网络流或高频计算场景中,这两个类型几乎是性能优化的标配。本文将从原理、区别到最佳实践,系统讲清它们的用法。
什么是 Span<T>?
Span<T> 是一种表示连续内存区域的轻量级类型,可以指向:
- 数组
- 栈内存(stackalloc)
- 非托管内存
它本质上是一个内存视图,不会复制数据。
Span<T> 的核心特点
- 零分配(Zero Allocation)
- 支持切片(Slice)但不复制数据
- 只能存在于栈上(ref struct)
示例:
byte[] data = GetData();
Span<byte> slice = data.AsSpan(100, 50);
这里不会创建新数组,而只是引用原内存的一部分。
什么是 Memory<T>?
Memory<T> 是 Span<T> 的可扩展版本,可以在堆上存活,支持更复杂场景。
Memory<T> 的核心特点
- 可作为字段、返回值、参数
- 可跨 async/await
- 可长期持有
示例:
Memory<byte> memory = new byte[1024];
Span<byte> span = memory.Span;
Memory<T> 可以理解为可以长期存活的 Span 容器。
Span<T> vs Memory<T> 核心区别
| 特性 | Span<T> | Memory<T> |
|---|---|---|
| 存储位置 | 仅栈 | 栈 + 堆 |
| 是否可用于 async | 不可以 | 可以 |
| 是否可作为字段 | 不可以 | 可以 |
| 性能 | 更高 | 略低 |
| 使用方式 | 直接操作 | 通过 .Span |
关键本质:
- Span<T> = 极致性能(但受限制)
- Memory<T> = 更灵活(但略有开销)
官方也明确指出:Span<T> 不能用于异步或跨线程场景,而 Memory<T> 正是为此设计的补充。
为什么它们性能更强?
传统代码中:
var sub = array.Skip(100).Take(50).ToArray(); // 分配+复制
问题:
- 创建新数组
- 增加 GC 压力
- 数据复制耗时
而 Span<T>:
var sub = array.AsSpan(100, 50); // 无分配
优势:
- 避免堆分配
- 避免内存复制
- 减少 GC 频率
这些优化在高频场景(如 JSON 解析、网络通信)中尤为明显。
典型使用场景
1. 字符串与文本处理
ReadOnlySpan<char> span = "hello world".AsSpan();
var slice = span.Slice(0, 5);
适用于:
- JSON解析
- CSV处理
- 日志处理
2. 二进制协议解析
Span<byte> buffer = stackalloc byte[256];
- 网络协议
- 文件解析
3. 高性能 I/O(推荐 Memory<T>)
async Task ReadAsync(Stream stream, Memory<byte> buffer)
{
await stream.ReadAsync(buffer);
}
4. 管道与缓冲区(ASP.NET Core)
结合:
- IBufferWriter<T>
- PipeReader
可以实现零拷贝数据流。
最佳实践(重点)
1. 优先使用 Span<T>
如果满足同步方法、生命周期短、不跨线程,优先用 Span<T>(性能最佳)
2. 跨 async 用 Memory<T>
Task ProcessAsync(Memory<byte> buffer)
避免使用 Span<T>(会编译报错)
3. 使用 ReadOnlySpan<T>
ReadOnlySpan<char> text
优点:更安全、避免误修改。
4. 避免破坏零分配
以下操作要谨慎:
span.ToArray();
new string(span);
否则优化失效。
5. 配合 ArrayPool 使用
var array = ArrayPool<byte>.Shared.Rent(1024);
var span = array.AsSpan(0, length);
6. 小缓冲区用 stackalloc
Span<byte> buffer = stackalloc byte[256];
注意:不要过大(一般 < 1KB)。
7. API 设计建议
- 输入参数:ReadOnlySpan<T> / Memory<T>
- 输出数据:Memory<T>(避免返回 Span)
8. 注意生命周期问题(非常重要)
错误示例:
Span<byte> GetSpan()
{
Span<byte> s = stackalloc byte[10];
return s; // 危险
}
原因:Span 绑定栈帧,离开即失效。
常见误区
误区1:Span 能提升并发性能
事实:它只是减少分配,不涉及线程调度或锁机制
误区2:所有项目都需要 Span
现实:仅在性能瓶颈明确时使用,普通业务代码收益有限
误区3:Memory 比 Span 更慢
实际上:差距很小,在 async 场景必须使用 Memory
总结
Span<T> 与 Memory<T> 是 .NET 高性能开发的重要基石:
- Span<T>:极致性能,适合短生命周期同步代码
- Memory<T>:更灵活,适合异步和跨层传递
- 两者结合:实现零拷贝 + 低GC + 高吞吐
一句话总结:能用 Span 就用 Span,需要跨生命周期就用 Memory。