Span 与 Memory 高性能编程
大约 9 分钟约 2606 字
Span 与 Memory 高性能编程
简介
Span<T> 和 Memory<T> 是 .NET Core 引入的高性能内存操作类型,用于零拷贝地操作连续内存区域。它们避免了不必要的数组分配和字符串复制,在高频处理(如网络解析、文本处理、图像操作)中能显著提升性能并降低 GC 压力。
特点
Span<T> 基础
创建 Span
/// <summary>
/// Span<T> 的多种创建方式
/// </summary>
public class SpanCreation
{
public void Examples()
{
// 从数组创建
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span1 = array;
// 从数组的一部分创建
Span<int> span2 = array.AsSpan(1, 3); // [2, 3, 4]
// 从 stackalloc 创建(栈分配,无 GC)
Span<int> span3 = stackalloc int[100];
// 从字符串创建(ReadOnlySpan<char>)
ReadOnlySpan<char> text = "Hello World".AsSpan();
// 从指针创建(unsafe)
// Span<byte> span4 = new Span<byte>(pointer, length);
}
}Span 操作
/// <summary>
/// Span<T> 的常用操作
/// </summary>
public class SpanOperations
{
public void Examples()
{
Span<int> numbers = stackalloc int[] { 10, 20, 30, 40, 50 };
// 索引访问
int first = numbers[0]; // 10
int last = numbers[^1]; // 50
// 切片 — 零拷贝
Span<int> slice = numbers[1..4]; // [20, 30, 40]
// 修改(修改的是原始数据)
slice[0] = 99;
// numbers 现在是 [10, 99, 30, 40, 50]
// 清空
numbers.Clear();
// 填充
numbers.Fill(42);
// [42, 42, 42, 42, 42]
// 复制
Span<int> target = new int[5];
numbers.CopyTo(target);
// 反转
Span<int> data = stackalloc int[] { 1, 2, 3, 4, 5 };
data.Reverse(); // [5, 4, 3, 2, 1]
}
}文本处理优化
/// <summary>
/// 使用 ReadOnlySpan<char> 优化字符串操作
/// </summary>
public class TextProcessor
{
// 传统方式 — 多次分配新字符串
public (string protocol, string host, string port) ParseUrl_Old(string url)
{
var parts1 = url.Split("://");
var protocol = parts1[0];
var remaining = parts1[1];
var colonIndex = remaining.IndexOf(':');
var host = remaining.Substring(0, colonIndex);
var port = remaining.Substring(colonIndex + 1);
return (protocol, host, port);
}
// Span 方式 — 零分配
public (string protocol, string host, string port) ParseUrl(ReadOnlySpan<char> url)
{
var separator = "://".AsSpan();
var protoEnd = url.IndexOf(separator);
var protocol = url[..protoEnd];
var remaining = url[(protoEnd + 3)..];
var colonIndex = remaining.IndexOf(':');
var host = remaining[..colonIndex];
var port = remaining[(colonIndex + 1)..];
return (protocol.ToString(), host.ToString(), port.ToString());
}
// CSV 解析
public List<string> ParseCsvLine(ReadOnlySpan<char> line)
{
var result = new List<string>();
int start = 0;
for (int i = 0; i < line.Length; i++)
{
if (line[i] == ',')
{
result.Add(line[start..i].ToString());
start = i + 1;
}
}
result.Add(line[start..].ToString());
return result;
}
}Memory<T>
与 Span 的区别
/// <summary>
/// Memory<T> — 可以在堆上使用的内存抽象
/// 适用于 async 方法和类字段
/// </summary>
public class MemoryExamples
{
// Memory<T> 可以作为字段
private Memory<byte> _buffer;
// Memory<T> 可以用于 async 方法
public async Task ProcessAsync(Memory<byte> data)
{
// 在 await 之前获取 Span
ProcessSpan(data.Span);
await Task.Delay(100);
// await 之后仍可使用
ProcessSpan(data.Span);
}
private void ProcessSpan(Span<byte> data)
{
for (int i = 0; i < data.Length; i++)
{
data[i] = (byte)(data[i] ^ 0xFF); // 简单异或
}
}
}IMemoryOwner — 池化内存
/// <summary>
/// IMemoryOwner<T> — 使用 ArrayPool 管理内存
/// </summary>
public class PooledBuffer : IMemoryOwner<byte>
{
private readonly byte[] _array;
private readonly ArrayPool<byte> _pool;
public PooledBuffer(int size, ArrayPool<byte>? pool = null)
{
_pool = pool ?? ArrayPool<byte>.Shared;
_array = _pool.Rent(size);
Memory = _array.AsMemory(0, size);
}
public Memory<byte> Memory { get; }
public void Dispose()
{
_pool.Return(_array);
}
}
// 使用
public byte[] ProcessLargeData(int size)
{
using var buffer = new PooledBuffer(size);
FillData(buffer.Memory.Span);
return buffer.Memory.ToArray();
}实际应用场景
1. 网络协议解析
/// <summary>
/// 使用 Span 解析二进制协议
/// </summary>
public ref struct PacketReader
{
private readonly ReadOnlySpan<byte> _data;
private int _position;
public PacketReader(ReadOnlySpan<byte> data)
{
_data = data;
_position = 0;
}
public byte ReadByte() => _data[_position++];
public short ReadInt16()
{
var value = BinaryPrimitives.ReadInt16BigEndian(_data[_position..]);
_position += 2;
return value;
}
public int ReadInt32()
{
var value = BinaryPrimitives.ReadInt32BigEndian(_data[_position..]);
_position += 4;
return value;
}
public string ReadString(int length)
{
var str = Encoding.UTF8.GetString(_data.Slice(_position, length));
_position += length;
return str;
}
public ReadOnlySpan<byte> ReadBytes(int count)
{
var slice = _data.Slice(_position, count);
_position += count;
return slice;
}
}2. Base64 编码优化
/// <summary>
/// 使用 Span 优化 Base64 操作
/// </summary>
public static class Base64Optimized
{
// 传统方式 — 分配新数组
public static string EncodeTraditional(byte[] data)
{
return Convert.ToBase64String(data);
}
// Span 方式 — 可以直接在预分配缓冲区中操作
public static bool TryEncode(Span<byte> destination, ReadOnlySpan<byte> source, out int written)
{
return Convert.TryToBase64Chars(source,
System.Text.Encoding.ASCII.GetChars(destination), out written);
}
}3. 高性能搜索
/// <summary>
/// 使用 Span 实现高性能字符串搜索
/// </summary>
public static class SpanSearch
{
public static int IndexOf(this ReadOnlySpan<char> source, ReadOnlySpan<char> value)
{
if (value.Length == 0) return 0;
if (value.Length > source.Length) return -1;
for (int i = 0; i <= source.Length - value.Length; i++)
{
if (source.Slice(i, value.Length).SequenceEqual(value))
return i;
}
return -1;
}
}Span vs Memory 选择指南
| 场景 | 选择 | 原因 |
|---|---|---|
| 同步方法内部 | Span<T> | 性能最优,无分配 |
| async 方法 | Memory<T> | Span 不能跨 await |
| 类字段 | Memory<T> | Span 不能在堆上 |
| 方法参数(同步) | ReadOnlySpan<T> | 零拷贝只读访问 |
| 方法参数(异步) | ReadOnlyMemory<T> | 跨 await 安全 |
| 短生命周期缓冲 | stackalloc + Span | 栈分配零 GC |
| 大缓冲区 | ArrayPool + Memory | 池化复用 |
优点
缺点
Span 在 BCL 中的应用
string.Create 高性能构造
/// <summary>
/// string.Create — 直接写入目标字符串的字符缓冲区
/// </summary>
// 传统方式 — 多次分配中间字符串
public string FormatUser_Old(string name, int age, string city)
{
return string.Format("Name: {0}, Age: {1}, City: {2}", name, age, city);
// 内部使用 StringBuilder,可能有多次分配
}
// ✅ string.Create — 零中间分配
public string FormatUser(string name, int age, string city)
{
// 预计算长度
int length = 6 + name.Length + 6 + age.ToString().Length + 7 + city.Length;
return string.Create(length, (name, age, city), (span, state) =>
{
"Name: ".AsSpan().CopyTo(span);
int pos = 6;
state.name.AsSpan().CopyTo(span.Slice(pos));
pos += state.name.Length;
", Age: ".AsSpan().CopyTo(span.Slice(pos));
pos += 7;
state.age.TryFormat(span.Slice(pos), out int written);
pos += written;
", City: ".AsSpan().CopyTo(span.Slice(pos));
pos += 8;
state.city.AsSpan().CopyTo(span.Slice(pos));
});
}
// string.Create 的格式化重载(.NET 6+)
public string FormatWithInterpolation(string name, int age)
{
// DefaultInterpolatedStringHandler 优化
return $"Name: {name}, Age: {age}";
// 编译器自动优化为 string.Create 或 handler 模式
}Span 与集合操作
/// <summary>
/// Span 在集合操作中的使用
/// </summary>
// CollectionsMarshal.AsSpan — List<T> 转为 Span<T>
public void ProcessListWithSpan(List<int> list)
{
// 零分配获取 List 的底层 Span
Span<int> span = CollectionsMarshal.AsSpan(list);
// 修改 Span 会直接修改 List 的内容
for (int i = 0; i < span.Length; i++)
span[i] *= 2;
// list 中的值也被修改了
}
// CollectionsMarshal.GetValueRefOrNullRef — Dictionary 直接访问
public void UpdateDictionary(Dictionary<string, int> dict, string key, int newValue)
{
// 零分配获取值的引用
ref var value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
{
value = newValue; // 直接修改,无需再次查找
}
}
// Dictionary 与 Span 的值类型优化
// Dictionary<string, int> 的值是 int,在 ref struct 中可以安全使用
public ref struct DictionaryAccessor<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, TValue> _dict;
private TValue _value;
public DictionaryAccessor(Dictionary<TKey, TValue> dict, TKey key)
{
_dict = dict;
if (dict.TryGetValue(key, out _value))
HasValue = true;
else
HasValue = false;
}
public bool HasValue { get; }
public ref TValue Value => ref _value;
}Span 与文件 I/O
/// <summary>
/// 使用 Span 优化文件读写
/// </summary>
// 高性能文件读取
public async Task ProcessLargeFileAsync(string path)
{
using var file = File.OpenRead(path);
using var reader = new StreamReader(file);
// 使用固定大小的缓冲区
char[] buffer = new char[8192];
int bytesRead;
while ((bytesRead = await reader.ReadAsync(buffer)) > 0)
{
ReadOnlySpan<char> chunk = buffer.AsSpan(0, bytesRead);
ProcessChunk(chunk);
}
}
// 零分配的行解析
public void ProcessLinesWithSpan(string content)
{
ReadOnlySpan<char> remaining = content.AsSpan();
while (!remaining.IsEmpty)
{
int newLineIndex = remaining.IndexOf('\n');
ReadOnlySpan<char> line;
if (newLineIndex >= 0)
{
line = remaining[..newLineIndex].TrimEnd('\r');
remaining = remaining[(newLineIndex + 1)..];
}
else
{
line = remaining;
remaining = ReadOnlySpan<char>.Empty;
}
// 处理行 — 零分配
ParseLine(line);
}
}
// 使用 Memory<byte> 进行异步写入
public async Task WriteDataAsync(Stream stream, ReadOnlyMemory<byte> data)
{
await stream.WriteAsync(data);
}总结
Span<T> 和 Memory<T> 是 .NET 高性能编程的核心工具。同步场景优先用 Span,异步场景用 Memory。掌握它们的创建、切片、解析操作,是写出高性能 .NET 代码的关键。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《Span 与 Memory 高性能编程》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《Span 与 Memory 高性能编程》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Span 与 Memory 高性能编程》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Span 与 Memory 高性能编程》最大的收益和代价分别是什么?
