Span 是 .NET Core 2.1 引入的里程碑式类型,它提供了一种类型安全、无需额外分配的内存视图抽象,能够在不复制数据的前提下操作连续内存块。配合 Memory、ReadOnlySpan 和 stackalloc,开发者可以编写出接近 C/C++ 级别性能的托管代码。本文将深入探讨这些值类型的内存语义和典型应用。
Span 本质
1 2 3 4 5 6
| public readonly ref struct Span<T> { private readonly ref T _reference; private readonly int _length; }
|
关键特点:
- 栈上分配(ref struct),不产生 GC 压力
- 可引用数组、原生内存(IntPtr)、栈内存(stackalloc)
- 提供类似数组的切片(slice)语义而不复制数据
创建 Span
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int[] array = [1, 2, 3, 4, 5]; Span<int> span1 = array;
Span<int> slice = array.AsSpan(1, 3);
IntPtr ptr = Marshal.AllocHGlobal(100); Span<byte> nativeSpan; unsafe { nativeSpan = new Span<byte>(ptr.ToPointer(), 100); } Marshal.FreeHGlobal(ptr);
Span<byte> stackSpan = stackalloc byte[256]; Span<int> ints = stackalloc int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
Span 在字符串处理中的应用
避免 Substring 引起的新字符串分配:
1 2 3 4 5 6 7 8 9 10
| string fullName = "张三,工程师,北京"; string name = fullName.Substring(0, 2);
ReadOnlySpan<char> span = fullName.AsSpan(); ReadOnlySpan<char> nameSpan = span[..2]; ReadOnlySpan<char> titleSpan = span[3..6];
Console.WriteLine(nameSpan.ToString());
|
高性能解析 CSV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static void ParseCsvLine(ReadOnlySpan<char> line, Span<ReadOnlySpan<char>> fields) { int fieldIndex = 0; int start = 0;
for (int i = 0; i <= line.Length; i++) { if (i == line.Length || line[i] == ',') { if (fieldIndex < fields.Length) { fields[fieldIndex++] = line[start..i]; } start = i + 1; } } }
ReadOnlySpan<char> csvLine = "张三,28,工程师"; Span<ReadOnlySpan<char>> fields = stackalloc ReadOnlySpan<char>[3]; ParseCsvLine(csvLine, fields); Console.WriteLine(fields[0].ToString());
|
Memory —— 堆上版本的 Span
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| async Task ProcessDataAsync(Memory<byte> buffer) { Span<byte> span = buffer.Span; span[0] = 0xFF;
await WriteToNetworkAsync(buffer); }
static async ValueTask WriteToNetworkAsync(Memory<byte> data) { await Task.Delay(10); }
|
ReadOnlySequence —— 非连续内存
当数据分布在多个连续内存段(如网络缓冲区的链式结构)时使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| using System.Buffers;
ReadOnlySequence<byte> sequence;
struct HttpParser { public bool TryParseRequest(ref ReadOnlySequence<byte> buffer, out HttpRequest request) { var reader = new SequenceReader<byte>(buffer); return reader.TryReadTo(out ReadOnlySpan<byte> line, (byte)'\n'); } }
|
ArrayPool —— 数组池化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| using System.Buffers;
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); try { int bytesRead = await stream.ReadAsync(buffer); Span<byte> validData = buffer.AsSpan(0, bytesRead); Process(validData); } finally { ArrayPool<byte>.Shared.Return(buffer); }
|
ArrayPool 内部:维护了多个桶(bucket),每个桶按 2 的幂次大小(16, 32, 64, …)组织数组。请求 1024 时返回 1024 大小的数组(也可能稍大)。重用数组减少 GC Gen 1/2 回收频率。
实战:高性能 Base64 编码器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| using System.Buffers; using System.Buffers.Text;
public static int Base64Encode(ReadOnlySpan<byte> source, Span<char> destination) { return Base64.EncodeToUtf8(source, destination, out _, out _); }
public static int FastToHex(ReadOnlySpan<byte> bytes, Span<char> hex) { const string hexChars = "0123456789ABCDEF";
for (int i = 0; i < bytes.Length; i++) { byte b = bytes[i]; hex[i * 2] = hexChars[b >> 4]; hex[i * 2 + 1] = hexChars[b & 0xF]; } return bytes.Length * 2; }
|
unsafe 与 Span
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| unsafe { int* ptr = stackalloc int[100]; var span = new Span<int>(ptr, 100);
span[0] = 42;
fixed (int* arrayPtr = array) { var fixedSpan = new Span<int>(arrayPtr, array.Length); } }
|
实战:高性能日志格式化
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
| using System.Buffers; using System.Text;
public static class FastLogger { private static readonly SearchValues<char> NewLines = SearchValues.Create(['\n', '\r']);
public static string Format(string level, ReadOnlySpan<char> message) { var now = DateTime.UtcNow; int capacity = 30 + level.Length + message.Length;
using var builder = new ValueStringBuilder(stackalloc char[256]); builder.Append(now.ToString("yyyy-MM-dd HH:mm:ss")); builder.Append(" ["); builder.Append(level); builder.Append("] "); builder.Append(message);
return builder.ToString(); } }
|
常见性能误区
1 2 3 4 5 6 7 8 9 10 11 12 13
| Span<byte> buffer = stackalloc byte[1024 * 10];
byte[] poolBuffer = ArrayPool<byte>.Shared.Rent(10240); Span<byte> safeBuffer = poolBuffer;
object boxed = (object)new Span<int>();
Span<int> span = array; Action action = () => Console.WriteLine(span[0]);
|
性能数据对比
| 操作 |
传统方式 |
Span 方式 |
加速比 |
| 字符串 Substring |
51 ns (分配) |
0 ns (零分配) |
∞ |
| 数组切片 |
分配新数组 |
零分配切片 |
∞ |
| 解析 CSV 1000 行 |
~500 μs |
~50 μs |
~10x |
| Base64 编码 |
分配 byte[] |
ArrayPool + Span |
~3x |
| 字节反转 |
分配新数组 |
原地修改 Span |
~2x + 零分配 |
总结:Span<T> 使 C# 无需 unsafe 代码就能写出接近原生的高性能内存操作代码。零分配切片、stackalloc、ArrayPool<T> 的组合大幅降低了 GC 压力。配合 Memory<T> 和 ReadOnlySequence<T>,即使在异步和高性能网络场景下也能游刃有余。掌握 Span-based 编程是进阶高性能 .NET 开发者的必备技能。