深入 .NET Memory<T>:Span 之后,为什么还需要一个 Memory?

.NET 高性能编程里有一对孪生兄弟——Span<T>Memory<T>。两个看起来都在"包一段内存",性能都说自己是"零拷贝、零分配",但只要写到一处 await,IDE 就开始抱怨:"Span<T> 是 ref struct,不能在这里用。"

这就是 Memory<T> 存在的全部动机——

Span<T> 是栈上的极致性能,但它有枷锁;Memory<T> 是它的"跨边界护照",让零拷贝内存能安全地穿越 await、字段、IEnumerable<T> 这些"危险区域"。

理解这层关系,比记住一堆 API 重要。本文围绕这条主线,把 Memory<T> 从底层布局到使用陷阱彻底讲透。


一、回到 Span:极致性能的代价

要看懂 Memory,先看 Span 解决了什么、付出了什么。

1.1 没有 Span 之前

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 想对"一段连续数据"做处理,但来源五花八门:
void ProcessArray(byte[] arr, int offset, int len) { ... }
void ProcessString(string s, int offset, int len) { ... }
void ProcessPointer(IntPtr ptr, int len) { ... }

// 调用方:
ProcessArray(buffer, 10, 5);
ProcessString(text, 1, 3);

// 问题:
//   - 每种来源要写一份重复代码
//   - 调用方需要传 (array, offset, length) 三元组,容易出错
//   - string 内部是 UTF-16,byte[] 是字节, IntPtr 是非托管,三者之间不能互通

1.2 Span 的设计:内存的"通用切片"

Span<T> 的内部其实就是两个字段:

1
2
3
4
5
public readonly ref struct Span<T>
{
    private readonly ref T _pointer;   // 对内存的引用(byref)
    private readonly int _length;
}
1
2
3
4
5
6
7
关键点:
  1. ref T _pointer:不是普通指针,是"指向 T 的引用"(managed pointer)
     → 既能指向托管数组、也能指向 stackalloc、还能指向 native heap
     → GC 知道它在引用什么,不会被错判回收
  2. int _length:长度

  Span<T> 本体只是 16 字节(64 位下两个指针大小)

1.3 ref struct 的枷锁

Span<T>ref struct,这个约束带来两个硬限制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
❌ 不能作为 class 的字段
❌ 不能装箱(box) → 不能作为 object
❌ 不能跨 await / yield
❌ 不能实现 IDisposable 之外的接口
❌ 不能作为泛型参数 T(除非加 unmanaged 约束等)
❌ 不能被 lambda 捕获

为什么这么严?
  - ref T _pointer 是"内部指针"
  - 一旦允许装箱,就可能逃逸到堆
  - 一旦逃逸到堆,引用的目标(可能是 stackalloc 的栈内存)可能已经失效
  → 引用悬空 → GC 灾难 → 类型安全崩塌

  所以 ref struct 必须"留在栈上",编译期阻止任何逃逸。

1.4 痛点:写异步代码时处处碰壁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public async Task ProcessAsync(byte[] buffer)
{
    Span<byte> span = buffer.AsSpan();

    // ❌ 编译错误:Span 不能作为 lambda 字段
    await Task.Run(() => {
        // ... span ...
    });

    // ❌ 编译错误:Span 不能跨 await
    await DoSomethingAsync();
    Console.WriteLine(span.Length);

    // ❌ 编译错误:Span 不能作为字段
    // class Holder { public Span<int> Data; }
}

但这正是现代 .NET 异步编程的主战场——网络、文件、Pipe、Stream 全是异步。这里性能瓶颈也最重——一次性 byte[] 分配堆内存,进 GC,进 LOH,吞吐量直接掉。

需要一个东西:像 Span 一样零拷贝、但又能在异步边界安全传递。这就是 Memory<T>


二、Memory 的设计:可装箱的"内存护照"

2.1 一句话定义

Memory<T>Span<T> 的"可装箱、可跨 await"版本。它不持有 ref T,而是持有"提取 Span 的能力",所以可以放在堆上。

2.2 内部布局

1
2
3
4
5
6
public readonly struct Memory<T>
{
    private readonly object _object;     // 内存来源(可能是 T[]、string、MemoryManager<T>)
    private readonly int _index;         // 起始偏移
    private readonly int _length;        // 长度
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
和 Span 的核心差别:
  Span<T>:ref T _pointer   ← GC 直接跟踪
  Memory<T>:object _object  ← 通过对象间接引用

为什么这样设计?
  - 不持有 ref,所以 Memory 是普通 struct(不是 ref struct)
  - 可以装箱、可以跨 await、可以作为字段
  - 但 Span 暂时拿不到 → 需要"提取"

  Span 怎么提取?
    Span<T> span = memory.Span;   ← 这个 getter 内部做"安全提取"

2.3 为什么 Memory 不直接持有 ref

1
2
3
4
5
6
7
假设 Memory<T> 持有 ref T _pointer:
  - Memory 是 struct,可以装箱
  - 装箱后 ref T 留在堆上
  - 如果原数据是 stackalloc → 装箱后栈释放 → ref 悬空 → 崩溃

→ Memory 设计上就不允许持有 ref,只能持有 object
→ 真要拿 Span 时,从 object 中提取,提取时编译器/运行时知道这是同步操作,安全

2.4 三种合法的 _object 类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Memory<T>._object 只可能是三种:

1. T[] 数组
   - new int[100].AsMemory()
   - GC 知道这是托管堆上的数组,安全

2. string(仅当 T 为 char)
   - "hello".AsMemory()
   - string 不可变 → ReadOnlyMemory<char>

3. MemoryManager<T>(自定义)
   - 例如 MemoryManager<char> 包装 native 内存
   - 自定义 Pin() 让 GC 知道何时 pin

4. null(表示空 Memory 或 default)
1
2
3
4
5
6
7
8
9
// 三种典型构造方式
byte[] arr = new byte[100];
Memory<byte> m1 = arr.AsMemory();          // _object = arr

string s = "hello";
ReadOnlyMemory<char> m2 = s.AsMemory();    // _object = s

// 自定义(不常见)
Memory<int> m3 = MemoryMarshal.CreateFromPinnedArray(arr, 0, 100);

三、Memory 与 Span 的转换:一次"安全握手"

3.1 Memory → Span

1
2
Memory<byte> memory = new byte[1024];
Span<byte> span = memory.Span;       // ← 提取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
这一步是"零拷贝"的:
  - 内部根据 _object 类型生成 Span
  - 数组:span = new Span<T>((T[])_object, _index, _length)
  - string:span = ref Unsafe.AsRef(in s.GetFirstChar()) + offset
  - MemoryManager:调用 MemoryManager.GetSpan()

为什么这次"提取"是安全的?
  - 提取发生在栈上
  - 拿到的 Span<T> 不能逃逸(ref struct 约束)
  - 一旦 Span 用完,对象引用还在 Memory 里持有

3.2 Span → Memory:不能直接转

1
2
Span<byte> span = stackalloc byte[100];
Memory<byte> memory = span;  // ❌ 编译错误!

为什么不行?因为 Span 可能指向栈内存(stackalloc),如果转成 Memory 装箱到堆上,栈帧结束后引用悬空。

但如果是数组-backed 的 Span,可以转:

1
2
3
4
5
Span<byte> span = new byte[100];
Memory<byte> memory = span.ToArray();  // ⚠️ 拷贝了一份!

// 或者用 MemoryMarshal(不安全)
Memory<byte> mem = MemoryMarshal.AsMemory(span);  // ⚠️ 不推荐

3.3 Memory 的 Slice:返回 Memory

1
2
3
4
5
6
Memory<byte> mem = new byte[100];
Memory<byte> slice = mem.Slice(10, 20);

// 内部就是:
// return new Memory<T>(_object, _index + 10, 20);
// 仍然不持 ref,仍然可以装箱/跨 await

而 Span 的 Slice:

1
2
3
4
5
Span<byte> span = mem.Span;
Span<byte> slice = span.Slice(10, 20);

// 内部是:return new Span<T>(ref _pointer[10], 20);
// 仍然是 ref struct

3.4 转换成本对比

操作成本
Memory<T>.Span 提取几纳秒(一次方法调用 + 类型判断)
Memory<T>.Slice16 字节 struct 创建
Span<T>.Slice16 字节 struct 创建
Memory<T>.ToArray()拷贝整个数组(O(n))
Memory<T>.Pin()创建 MemoryHandle,pin 对象

四、Memory 的三种来源

4.1 来自托管数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
byte[] buffer = new byte[4096];
Memory<byte> mem = buffer.AsMemory();

// 切片
Memory<byte> slice = mem.Slice(0, 1024);

// 跨 await
await ProcessAsync(slice);

async Task ProcessAsync(Memory<byte> data)
{
    // 在 await 前后都能用
    Span<byte> span = data.Span;
    span[0] = 0xFF;
    await Task.Yield();
    Console.WriteLine(span[0]);  // 0xFF
}

4.2 来自字符串(仅 char)

1
2
3
4
5
6
7
8
string text = "Hello, World!";
ReadOnlyMemory<char> mem = text.AsMemory();

ReadOnlyMemory<char> word = mem.Slice(7, 5);
Console.WriteLine(word.ToString());  // "World"

// 跨 await 安全
await ProcessAsync(word);
1
2
3
4
注意:
  - string 是不可变的 → ReadOnlyMemory<char>
  - 不能改成 Memory<char>
  - 想要可变字符串用 StringBuilder 或 char[]

4.3 来自 MemoryManager(自定义)

MemoryManager<T> 是抽象类,让你包装"非标准内存"为 Memory<T>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public sealed class NativeMemoryManager<T> : MemoryManager<T>
{
    private unsafe T* _ptr;
    private readonly int _length;

    public unsafe NativeMemoryManager(T* ptr, int length)
    {
        _ptr = ptr;
        _length = length;
    }

    public override Span<T> GetSpan()
    {
        unsafe => new Span<T>(_ptr, _length);
    }

    public override MemoryHandle Pin(int elementIndex = 0)
    {
        unsafe => new MemoryHandle(_ptr + elementIndex, default, this);
    }

    public override void Unpin() { /* native 内存不需要 pin */ }

    protected override void Dispose(bool disposing)
    {
        // 释放 native 内存
    }
}

// 用法:
unsafe
{
    T* ptr = (T*)NativeMemory.Allocate(size);
    var manager = new NativeMemoryManager<T>(ptr, size);
    Memory<T> mem = manager.Memory;
    // 之后像普通 Memory 一样用
}
1
2
3
4
5
6
7
MemoryManager<T> 的常见应用:
  - 包装 native 内存(malloc、mmap)
  - 包装 unmanaged 集合
  - 包装内存映射文件
  - 包装 GC pin 的对象

ASP.NET Core、Pipelines 内部都用 MemoryManager 包装底层缓冲区。

五、IMemoryOwner:所有权管理

5.1 为什么需要"所有者"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
问题:Memory<T> 是 struct,不持有"释放"概念
     谁该释放底层 buffer?

场景:
  - 你从 ArrayPool 租了一个 buffer
  - 多个方法共享 Memory<byte>
  - 用完应该归还给 Pool

  Memory<byte> 本身没有 Return() 方法
  → 需要"所有者"对象负责归还

5.2 IMemoryOwner 接口

1
2
3
4
public interface IMemoryOwner<T> : IDisposable
{
    Memory<T> Memory { get; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
谁实现:
  - MemoryPool<T>.Shared.Rent() 返回 IMemoryOwner<T>
  - 自定义实现(包装 ArrayPool

使用模式:
  using var owner = MemoryPool<byte>.Shared.Rent(1024);
  Memory<byte> mem = owner.Memory;

  // 传给其他方法(不需要释放责任)
  await ProcessAsync(mem);

  // using 自动释放(归还给 Pool

5.3 IMemoryOwner 的实现示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public sealed class ArrayPoolOwner<T> : IMemoryOwner<T>
{
    private readonly T[] _array;
    private readonly ArrayPool<T> _pool;

    public ArrayPoolOwner(T[] array, ArrayPool<T> pool)
    {
        _array = array;
        _pool = pool;
        Memory = array;
    }

    public Memory<T> Memory { get; }

    public void Dispose() => _pool.Return(_array);
}

// 用法:
public IMemoryOwner<byte> GetBuffer(int size)
{
    var array = ArrayPool<byte>.Shared.Rent(size);
    return new ArrayPoolOwner<byte>(array, ArrayPool<byte>.Shared);
}
1
2
3
4
5
设计哲学:
  - Memory<T> = "使用权"(可以读、可以切片、可以传递)
  - IMemoryOwner<T> = "所有权"(谁负责释放)

  类似 C++ 的 unique_ptr / shared_ptr 模式

六、MemoryPool 与 ArrayPool:池化是关键

6.1 为什么池化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
每次 new byte[4096] 的代价:
  1. 分配(堆上找空间)
  2. GC 跟踪
  3. 触发 GC 时回收
  4. 大数组(≥85000 字节)进 LOH,GC 代际高、回收慢

  - Web 服务器每秒 1000 请求 × 每请求 4KB buffer
    = 4MB/s 分配 → 几秒就触发一次 Gen2 GC

池化的核心:
  - 租一个 buffer(Rent):池里有就用,没有就 new
  - 用完归还(Return):放回池,下次复用
  - GC 完全不参与

6.2 ArrayPool:池化的底层

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 租
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
// 注意:实际返回的数组可能比请求的大(向上取整)
// 长度看 buffer.Length,不是 4096

try
{
    // 用 buffer
    int read = await stream.ReadAsync(buffer, 0, 4096);
}
finally
{
    // 归还
    ArrayPool<byte>.Shared.Return(buffer);

    // 如果数组被修改过且不需要清理,加 clearArray: false(默认)
    ArrayPool<byte>.Shared.Return(buffer, clearArray: false);
}
1
2
3
4
5
6
7
8
ArrayPool.Shared 的实现:
  - 每个桶(bucket)对应一种大小的数组
  - 线程本地缓存 + 全局共享
  - 默认大小:16, 32, 64, ..., 16384, 32768, ..., 1GB

注意:
  - 不能保存对归还后的数组的引用(你已不再"拥有"它)
  - 不能假设 Rent 的长度正好等于请求长度

6.3 MemoryPool:池化的 Memory 包装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// MemoryPool<T> 是 ArrayPool<T> 的"Memory 化"包装
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
Memory<byte> mem = owner.Memory;

// mem.Length 可能 > 4096(实际数组大小)
// 切片到精确大小
Memory<byte> exact = mem.Slice(0, 4096);

await ProcessAsync(exact);

// using 自动归还
1
2
3
4
5
6
7
MemoryPool<T>.Shared = MemoryPool<T>.Create ArrayPool 内部包装

实际差别:
  - ArrayPool<byte>.Rent() → byte[] (要自己管 finally)
  - MemoryPool<byte>.Rent() → IMemoryOwner<byte> (using 即可)

  推荐:MemoryPool 更现代化、更适合异步代码

6.4 池化的坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 坑 1:使用了归还后的数组
byte[] buf = ArrayPool<byte>.Shared.Rent(100);
ArrayPool<byte>.Shared.Return(buf);
buf[0] = 1;  // ⚠️ buf 已不属于你,可能被别人覆盖

// 坑 2:在 using 之后还使用 Memory
async Task BadAsync()
{
    Memory<byte> mem;
    using (var owner = MemoryPool<byte>.Shared.Rent(100))
    {
        mem = owner.Memory;  // 拿到引用
    }  // ← owner 已 Dispose,buffer 已归还!
    mem.Span[0] = 1;  // ⚠️ 未定义行为
}

// 坑 3:多线程同时 Rent + Return 同一数组
//      需要协议保证只在使用期间持有

// 坑 4:忘记 Return(内存泄漏)
//      ArrayPool 不会自动回收,必须显式 Return

七、实战场景

7.1 高性能 IO:Stream.ReadAsync(Span) vs ReadAsync(Memory)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ❌ 同步 Span 版本:不能跨 await
public byte[] ReadAll(Stream stream)
{
    using var ms = new MemoryStream();
    Span<byte> buf = stackalloc byte[4096];
    int read;
    while ((read = stream.Read(buf)) > 0)
    {
        ms.Write(buf.Slice(0, read));
    }
    return ms.ToArray();
}

// ✅ 异步 Memory 版本
public async Task<byte[]> ReadAllAsync(Stream stream)
{
    using var ms = new MemoryStream();
    using var owner = MemoryPool<byte>.Shared.Rent(4096);
    Memory<byte> buf = owner.Memory;

    int read;
    while ((read = await stream.ReadAsync(buf)) > 0)
    {
        ms.Write(buf.Span.Slice(0, read));
    }
    return ms.ToArray();
}
1
2
3
4
5
6
Stream.ReadAsync 的演进:
  .NET Framework:ReadAsync(byte[], offset, count)
  .NET Core 2.1+:ReadAsync(Memory<byte>) 扩展方法(基于 ValueTask)
  .NET Standard 2.1+:原生支持

  推荐用 Memory 重载,零分配 + ValueTask 友好

7.2 文本解析器:避免 LOH

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 大文件逐行解析(避免一次性 ReadAllText)
public async IAsyncEnumerable<(int Line, string Text)> ParseLinesAsync(
    Stream stream,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    using var owner = MemoryPool<byte>.Shared.Rent(64 * 1024);
    Memory<byte> buf = owner.Memory;

    int buffered = 0;
    int lineNo = 0;

    while (true)
    {
        int read = await stream.ReadAsync(buf.Slice(buffered), ct);
        if (read == 0) break;
        buffered += read;

        // 找换行
        var data = buf.Slice(0, buffered);
        int idx;
        while ((idx = data.Span.IndexOf((byte)'\n')) >= 0)
        {
            lineNo++;
            var line = data.Slice(0, idx);
            yield return (lineNo, Encoding.UTF8.GetString(line.Span));
            data = data.Slice(idx + 1);
        }

        // 剩余的移到 buf 开头
        data.Span.CopyTo(buf.Span);
        buffered = data.Length;
    }
}

7.3 协议解析:Memory 切片传递

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 解析二进制协议(如 MessagePack / Protobuf)
public class ProtocolReader
{
    private readonly Stream _stream;
    private readonly IMemoryOwner<byte> _owner;
    private Memory<byte> _buffer;

    public ProtocolReader(Stream stream, int bufferSize = 4096)
    {
        _stream = stream;
        _owner = MemoryPool<byte>.Shared.Rent(bufferSize);
        _buffer = _owner.Memory;
    }

    public async Task<ReadOnlyMemory<byte>> ReadMessageAsync()
    {
        // 读 header(4 字节长度)
        await ReadExactAsync(_buffer.Slice(0, 4));
        int length = BitConverter.ToInt32(_buffer.Span.Slice(0, 4));

        // 读 body
        if (length > _buffer.Length)
            throw new InvalidOperationException("Message too large");

        var body = _buffer.Slice(4, length);
        await ReadExactAsync(body);
        return body;
    }

    private async Task ReadExactAsync(Memory<byte> dst)
    {
        int total = 0;
        while (total < dst.Length)
        {
            int read = await _stream.ReadAsync(dst.Slice(total));
            if (read == 0) throw new EndOfStreamException();
            total += read;
        }
    }
}

7.4 ASP.NET Core 内部的 Memory

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ASP.NET Core 大量用 Memory<T>:
  - HttpContext.Request.Body : Stream,内部用 PipeReader
  - PipeReader 读出 ReadOnlySequence<byte>(一堆 Memory 拼接)
  - Response.Body.WriteAsync(ReadOnlyMemory<byte>)
  - SignalR 传输用 Memory<byte>

System.IO.Pipelines(Pipe / PipeReader / PipeWriter):
  - 微软的"高性能 IO 抽象"
  - 替代 Stream 的"读一点写一点"模式
  - 内部全部 Memory<T> + IMemoryOwner<T>

八、ReadOnlyMemory 与可变性

1
2
3
4
5
6
7
8
9
Memory<byte> mem = new byte[100];
ReadOnlyMemory<byte> rom = mem;       // 隐式转换

// ReadOnlyMemory 不能改
rom.Span[0] = 1;  // ❌ 编译错误

// 但底层是同一个数组!
mem.Span[0] = 1;
Console.WriteLine(rom.Span[0]);  // 1
1
2
3
4
5
6
7
8
观察:
  - ReadOnlyMemory<T> ≠ 不可变数据
  - 只是你"看不到"修改权限
  - 实际数据可能被另一个 Memory 引用修改

真正不可变的:
  - ImmutableArray<T>
  - 用 ToArray() 拷贝出来的副本

九、常见陷阱与最佳实践

9.1 持有 Memory 实际持有大数组

1
2
3
4
5
6
7
8
9
// 危险:Memory 是"切片",但底层对象是整个数组
byte[] big = new byte[10_000_000];
Memory<byte> small = big.AsMemory(0, 10);

// small 看起来只有 10 字节,但 big 不能被 GC 回收!
return small;  // 把 small 当返回值 → 大数组泄露

// 解决:用 ToArray()(如果数据真的小)
return small.ToArray();
1
2
3
4
Memory<T>.Pin() 也会 pin 整个数组:
  var handle = mem.Pin();
  //  handle Dispose 之前,整个底层数组被 GC pin
  // 不能移动、不能回收

9.2 跨 Method边界传递 Span

1
2
3
4
5
6
7
8
// ❌ 高性能反模式
public void Process(Span<byte> data) { ... }

// 如果 data 来自 stackalloc,调用方的栈结束后引用悬空
// ref struct 保证不会逃逸,但每次调用要重新算

// ✅ 改用 Memory
public Task ProcessAsync(Memory<byte> data) { ... }

9.3 用 ReadOnlyMemory 表达契约

1
2
3
4
5
6
// 表达"我只读不改"的契约
public Task<int> ParseAsync(ReadOnlyMemory<byte> data);

// 调用方既能传 Memory 也能传 ReadOnlyMemory
Memory<byte> m = ...;
await ParseAsync(m);  // 隐式转换

9.4 避免 Memory 装箱

1
2
3
4
5
6
// Memory 是 struct,但装箱到 object 仍然是性能损失
object boxed = (object)mem;  // ⚠️ 装箱
async Task<object> GetBoxedAsync() => mem;  // ⚠️ 装箱

// 直接用 Memory<T>,不要用 object
async Task<Memory<byte>> GetAsync() => mem;

9.5 验证生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public async Task BadAsync()
{
    Memory<byte> mem;
    using (var owner = MemoryPool<byte>.Shared.Rent(100))
    {
        mem = owner.Memory;
        // ❌ 把 mem 注册到回调,回调在 using 之后执行
        RegisterCallback(() => {
            mem.Span[0] = 1;  // 已归还的 buffer!
        });
    }
}

// ✅ 正确:所有使用都在 using 范围内
public async Task GoodAsync()
{
    using var owner = MemoryPool<byte>.Shared.Rent(100);
    Memory<byte> mem = owner.Memory;
    await ProcessAsync(mem);  // 整个 await 都在 using 范围内
}

十、MemoryMarshal:不安全边界

MemoryMarshal 提供"破坏封装"的低级操作,只在性能关键路径上用

10.1 AsBytes:把 Memory 当 byte 看

1
2
3
4
5
6
7
int[] ints = new int[] { 1, 2, 3 };
Memory<int> mi = ints;

Memory<byte> mb = MemoryMarshal.AsBytes(mi);
// mb 是 12 字节,包含 ints 的原始字节

// 用途:序列化、CRC、二进制协议

10.2 Cast:类型重新解释

1
2
3
4
5
6
Memory<byte> bytes = new byte[16];
Memory<int> ints = MemoryMarshal.Cast<byte, int>(bytes);
// ints 现在是 4 个 int,指向同一块内存

// 注意:TFrom 和 TTo 大小必须能整除
// byte (1) → int (4):16/4 = 4 个 int

10.3 TryGetArray:拿到原数组引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Memory<byte> mem = new byte[100];

if (MemoryMarshal.TryGetArray(mem, out ArraySegment<byte> seg))
{
    // seg.Array 是原数组
    // seg.Offset 是偏移
    // seg.Count 是长度

    // 用途:调用旧 API(如 Stream.Read(byte[], offset, count))
}
1
2
3
4
5
6
TryGetArray 失败的场景:
  - Memory 来自 string(仅 char)
  - Memory 来自 MemoryManager<T>
  - 没有底层托管数组

→ 失败时需要 fallback:拷贝到临时数组

10.4 CreateFromPinnedArray:pin 住

1
2
3
4
5
6
7
byte[] arr = new byte[100];
Memory<byte> mem = MemoryMarshal.CreateFromPinnedArray(arr, 0, 100);

// 创建时数组已被 pin
// mem.Span 拿到的指针不会被 GC 移动

// 用途:与 native 代码互操作(P/Invoke)

十一、性能基准

用 BenchmarkDotNet 看 Span vs Memory vs Array:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 基准测试代码
[MemoryDiagnoser]
public class MemBench
{
    private byte[] _data = new byte[1024];

    [Benchmark]
    public int SumArray()
    {
        int sum = 0;
        for (int i = 0; i < _data.Length; i++) sum += _data[i];
        return sum;
    }

    [Benchmark]
    public int SumSpan()
    {
        int sum = 0;
        foreach (var b in _data.AsSpan()) sum += b;
        return sum;
    }

    [Benchmark]
    public int SumMemory()
    {
        int sum = 0;
        foreach (var b in _data.AsMemory().Span) sum += b;
        return sum;
    }
}
1
2
3
4
5
6
7
8
9
典型结果(.NET 8):
  SumArray  : ~150 ns
  SumSpan   : ~120 ns  ← 最快(少了边界检查)
  SumMemory : ~125 ns  ← 几乎和 Span 一样(提取 Span 后循环)

观察:
  - Span 和 Memory 性能几乎一致(同步路径下)
  - Array 比 Span 略慢(边界检查开销)
  - Memory 的提取开销可忽略(仅一次方法调用)

十二、小结

本文从 Span<T> 的"栈枷锁"出发,系统讲了 Memory<T>

  • Memory<T> 的内部布局:object + index + length(不持 ref)
  • 为什么这样设计:可装箱、可跨 await、可作为字段
  • 三种来源:T[] 数组、string、MemoryManager
  • 与 Span 的转换:Memory.Span 提取、不能反向
  • IMemoryOwner<T> 的所有权语义
  • ArrayPool<T>MemoryPool<T> 的池化机制
  • 实战场景:异步 IO、文本解析、协议解析、ASP.NET Core 内部
  • 常见陷阱:持有切片但底层数组泄露、生命周期错位、装箱
  • MemoryMarshal 的不安全操作
1
2
3
4
记住三句话:
  1. Span 是栈上的极致性能,Memory 是它的"跨边界护照"
  2. Memory<T>.Span 是一次"安全握手",提取后 Span 仍然不能逃逸
  3. 池化(ArrayPool/MemoryPool)是高性能 .NET 的标配——少了它就是每秒万次的 GC

下次写异步 IO 或文本解析时,先用 MemoryPool<T>.Shared.Rent() + using,性能立刻上一个台阶。