C#怎么使用Span和Memory C#高性能内存操作指南

4次阅读

Span和Memory是C#7.2引入的高性能内存抽象,前者为上零分配视图,后者为可跨作用域的托管视图,二者均避免分配、减少GC压力,提升数据处理效率。

C#怎么使用Span和Memory C#高性能内存操作指南

Span 和 Memory 是 C# 7.2 引入的核心高性能内存抽象类型,专为避免堆分配、减少 GC 压力、提升数据处理效率而设计。它们不是“替代数组”,而是更安全、更灵活的“内存视图”——能指向内存、堆数组、本机内存甚至只读数据,且全程不触发装箱或额外拷贝。

Span:栈友好的零分配切片工具

Span 是 ref-like 类型,只能在栈上声明(如局部变量、方法参数),不能作为字段、不能被装箱、不能跨 await 边界。它的价值在于对已有内存做“零成本切片”和“就地操作”。

  • 从数组创建:Span span = Array.AsSpan(); —— 不复制,只是引用起始地址+长度
  • 切片操作:span.Slice(2, 5) —— O(1) 时间,返回新 Span,原数据不动
  • 配合 stackalloc:Span buffer = stackalloc byte[256]; —— 栈上分配,无 GC 开销
  • 写入字符串字节(UTF8):Utf8Encoder.Encode(buffer, “hello”, out int written);

Memory:可跨作用域的托管内存视图

Memory 是 Span 的“托管友好版”,可作为字段、参数、返回值,甚至用于 async 方法。它背后封装了 IMemoryOwner 或数组等资源,但本身不拥有内存。

  • 从数组创建:Memory mem = array;array.AsMemory()
  • 获取可写 Span:Span span = mem.Span; —— 安全转为栈视图操作
  • 异步场景示例:async Task ProcessAsync(Memory data) —— 调用方传入,方法内用 .Span 处理
  • 注意:Memory 本身不可序列化,也不直接暴露指针,安全性高于裸指针

常见高性能组合模式

真实项目中,Span/Memory 往往和特定 API 协同使用,才能释放最大效能。

  • 字符串解析(不用 Split/ToArray):ReadOnlySpan 遍历、IndexOf、Slice,跳过所有字符串分配
  • 二进制协议处理:Socket.ReceiveAsync 返回 Memory,直接用 BinaryPrimitives 读取 int/long,避免 ArraySegment 封装开销
  • 池化 + Memory:搭配 ArrayPool.Shared.Rent() 获取数组 → 转为 Memory → 处理完 Return(),复用缓冲区
  • 只读场景优先用 ReadOnlySpan/ReadOnlyMemory编译器会阻止意外写入,语义清晰,性能一致

必须避开的坑

Span 和 Memory 强大,但违反规则会导致编译失败或运行时异常(如 System.SpanHelpers.ThrowinvalidOperationIfNotInitialized)。

  • Span 不能存为类字段 —— 编译器直接报错:“Cannot declare a variable of type ‘Span‘ in a context where it cannot be used”
  • 不能在 async 方法中 await 后继续用之前捕获的 Span —— 因为可能跨栈帧,生命周期失控;应改用 Memory
  • stackalloc 分配过大(如 >1MB)可能栈溢出 —— 生产环境慎用超大 stackalloc,优先考虑 ArrayPool
  • 不要把 Span 暴露给不受信代码 —— 它可绕过部分数组边界检查(虽仍受 CLR 内存保护)

基本上就这些。Span 和 Memory 不是炫技工具,而是当你真在处理高频日志解析、网络包拆包、图像像素遍历、jsON 流式反序列化等场景时,最值得投入理解的底层利器。用对了,性能提升常是 2–5 倍,GC 次数直线下降。

text=ZqhQzanResources