C#中的高性能内存操作的利器:Span<T>和Memory<T>

 更新时间:2025年08月02日 10:32:51   作者:子丶不语  
在.NET开发中,内存管理一直是影响性能的关键因素,.NET Core 2.1引入Span和Memory优化内存管理,减少分配与复制开销,Span栈分配、无GC压力,适用于同步高性能场景;Memory堆分配、支持异步操作,适合跨方法传递与长期存储,合理选择可提升代码效率与可靠性

在.NET开发中,内存管理一直是影响性能的关键因素。传统的字符串处理、数组操作等往往伴随着大量的内存分配和复制操作,这些不必要的开销在高性能场景下尤为明显。

为了解决这个问题,.NET Core 2.1引入了Span和Memory这两个强大的类型,它们能够:

  • 显著减少内存分配
  • 提升数据操作性能
  • 安全地访问连续内存区域
  • 支持多种内存来源的统一操作

Span:栈上分配的高性能利器

Span的本质

Span是一个栈分配的结构体(值类型),它提供了一种不需要额外内存分配就能操作连续内存区域的方法。

int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers; 
span[0] = 10; 
Console.WriteLine(numbers[0]);

注意:数组堆上分配的引用类型,与Span还是有区别的,Span无GC压力。

Span与字符串处理

传统的字符串处理方法如Substring()会创建新的字符串实例,而使用Span可以避免这种额外的内存分配:

using System;

class Program
{
    static void Main()
    {
        string orderData = "ORD-12345-AB: 已发货";

        // 传统方式 - 创建新的字符串对象
        string orderId1 = orderData.Substring(0, 11); // 分配新内存
        string status1 = orderData.Substring(13);     // 再次分配新内存

        // 使用Span<T> - 不创建新的字符串对象
        ReadOnlySpan<char> dataSpan = orderData.AsSpan();
        ReadOnlySpan<char> orderId2 = dataSpan.Slice(0, 11); // 不分配新内存
        ReadOnlySpan<char> status2 = dataSpan.Slice(13);     // 不分配新内存

        // 必要时才将Span转换为string
        Console.WriteLine($"订单号: {orderId2.ToString()}");
        Console.WriteLine($"状态: {status2.ToString()}");
    }
}

使用stackalloc与Span

Span可以直接与栈上分配的内存一起使用,避免堆分配的开销:

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace AppSpanMemory
{
    internal class Program
    {
        static unsafe void Main()
        {
            Span<int> stackNums = stackalloc int[100];

            for (int i = 0; i < stackNums.Length; i++)
            {
                stackNums[i] = i * 10;
            }

            // 获取Span起始位置的指针
            void* ptr = Unsafe.AsPointer(ref MemoryMarshal.GetReference(stackNums));

            Console.WriteLine($"Span内存地址: 0x{(ulong)ptr:X}");

            // 打印前10个元素
            var firstTen = stackNums.Slice(0, 10);
            foreach (var n in firstTen)
            {
                Console.Write($"{n} ");
            }
            Console.ReadKey();
        }
    }
}

Span的关键特性

  • 零内存分配操作数据时不创建额外的内存对象
  • 类型安全提供类型检查,避免类型转换错误
  • 可用于多种内存来源数组、固定大小缓冲区、栈分配内存、非托管内存等
  • 性能优势适用于高性能计算和数据处理场景
  • 限制只能在同步方法中使用,不能作为类的字段

Memory:异步操作的理想选择

Memory的定位

Memory是Span的堆分配版本,主要用于支持异步操作场景。

// Memory<T>的基本使用
Memory<int> memory = new int[] { 1, 2, 3, 4, 5 };
Span<int> spanFromMemory = memory.Span; // 从Memory获取Span视图
spanFromMemory[0] = 20;
Console.WriteLine(memory.Span[0]);

Memory与异步文件操作

Memory在处理异步I/O操作时特别有用:

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace AppSpanMemory
{
    internal class Program
    {
        static async Task Main()
        {
            // 创建一个4KB的缓冲区
            byte[] buffer = new byte[4096];
            Memory<byte> memoryBuffer = buffer; 

            using FileStream fileStream = new FileStream("bigdata.dat", FileMode.Open, FileAccess.Read);
            int bytesRead = await fileStream.ReadAsync(memoryBuffer);

            if (bytesRead > 0)
            {
                Memory<byte> actualData = memoryBuffer.Slice(0, bytesRead);
                ProcessData(actualData.Span);
            }

            Console.WriteLine($"读取了 {bytesRead} 字节的数据");
        }

        static void ProcessData(Span<byte> data)
        {
            Console.WriteLine($"前10个字节: {BitConverter.ToString(data.Slice(0, Math.Min(10, data.Length)).ToArray())}");
        }
    }
}

Memory的关键特性

  • 异步友好可以在异步方法中使用
  • 不绑定执行上下文可以在方法之间传递
  • 可作为类字段可以存储在类中长期使用
  • 性能略低相比Span有轻微的性能开销
  • 更灵活可用于更多场景

Span与Memory的对比选择

特性

Span<T>

Memory<T>

分配位置

异步支持

不支持

支持

性能表现

更高

稍低

适用场景

同步高性能操作

异步操作、跨方法传递

可否作为字段

不可以

可以

生命周期

方法范围内

可长期存在

实战应用场景

高性能字符串解析

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace AppSpanMemory
{
    internal class Program
    {
        static async Task Main()
        {
            string csvLine = "张三,30,北京市海淀区,软件工程师";
            ParseCsvLine(csvLine.AsSpan());
        }

        public static void ParseCsvLine(ReadOnlySpan<char> line)
        {
            int start = 0;
            int fieldIndex = 0;

            for (int i = 0; i < line.Length; i++)
            {
                if (line[i] == ',')
                {
                    // 不创建新字符串
                    ReadOnlySpan<char> field = line.Slice(start, i - start);
                    ProcessField(fieldIndex, field);

                    start = i + 1;
                    fieldIndex++;
                }
            }

            // 处理最后一个字段
            if (start < line.Length)
            {
                ReadOnlySpan<char> lastField = line.Slice(start);
                ProcessField(fieldIndex, lastField);
            }
        }

        private static void ProcessField(int index, ReadOnlySpan<char> field)
        {
            Console.WriteLine($"字段 {index}: '{field.ToString()}'");
        }

    }
}

二进制数据处理

using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace AppSpanMemory
{
    internal class Program
    {
        static async Task Main()
        {
            string csvLine = "张三,30,北京市海淀区,软件工程师";

            byte[] payloadBytes = Encoding.UTF8.GetBytes(csvLine);

            // 头部4字节 + 数据长度4字节 + 数据体
            byte[] fileData = new byte[4 + 4 + payloadBytes.Length];

            // 写入头部标识 "DATA"
            fileData[0] = (byte)'D';
            fileData[1] = (byte)'A';
            fileData[2] = (byte)'T';
            fileData[3] = (byte)'A';

            // 写入数据长度(小端)
            BinaryPrimitives.WriteInt32LittleEndian(fileData.AsSpan(4, 4), payloadBytes.Length);

            // 写入数据体
            payloadBytes.CopyTo(fileData.AsSpan(8));

            // 传入文件字节数据的只读切片
            ProcessBinaryFile(fileData);
        }

        public static void ProcessBinaryFile(ReadOnlySpan<byte> data)
        {
            // [4字节头部标识][4字节数据长度][实际数据]
            if (data.Length < 8)
            {
                thrownew ArgumentException("数据格式不正确");
            }

            // 检查头部标识"DATA"
            ReadOnlySpan<byte> header = data.Slice(0, 4);
            if (!(header[0] == 'D' && header[1] == 'A' && header[2] == 'T' && header[3] == 'A'))
            {
                thrownew ArgumentException("无效的文件头");
            }

            // 读取数据长度 (小端字节序)
            int dataLength = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));

            // 确保数据完整
            if (data.Length < 8 + dataLength)
            {
                thrownew ArgumentException("数据不完整");
            }

            // 获取实际数据部分
            ReadOnlySpan<byte> payload = data.Slice(8, dataLength);

            Console.WriteLine($"有效载荷大小: {payload.Length} 字节");
            Console.WriteLine($"前10个字节: {BitConverter.ToString(payload.Slice(0, Math.Min(10, payload.Length)).ToArray())}");
        }

    }
}

使用注意事项

安全使用Span的建议

  • 不要尝试将Span作为字段存储
  • 不要将Span用于异步方法
  • 避免将Span装箱(boxing)
  • 小心Span的生命周期管理,特别是使用stackalloc时
  • 使用ReadOnlySpan表示不需要修改的数据

Memory的最佳实践

  • 优先考虑ReadOnlyMemory而非Memory(当不需要修改数据时)
  • 在异步操作中使用Memory替代数组
  • 在需要长期保留引用时使用Memory而非Span
  • 需要操作时才调用.Span属性,不要过早转换

兼容性与平台支持

Span和Memory支持情况:

  • .NET Core 2.1及更高版本
  • .NET Standard 2.1
  • .NET 5/6/7/8及以后版本
  • 不完全支持.NET Framework,但可通过System.Memory NuGet包获得部分支持

总结

Span和Memory是C#中处理高性能内存操作的强大工具,它们能够:

  1. 减少内存分配和GC压力通过避免不必要的内存分配和复制
  2. 提高性能特别是在处理大量数据和频繁字符串操作时
  3. 保持类型安全避免了使用unsafe代码和指针操作的风险
  4. 简化代码提供了直观的API来处理连续内存区域

在实际开发中,记住这些简单的选择规则:

  • 对于同步方法中的高性能操作,选择Span
  • 对于异步方法或需要跨方法传递的场景,选择Memory

掌握这两个强大的工具,将帮助你编写更高效、更可靠的C#代码,特别是在处理大数据量、高性能要求的应用场景中。

相关文章

  • C# GDI在控件上绘图的方法

    C# GDI在控件上绘图的方法

    这篇文章主要介绍了C# GDI在控件上绘图的方法,包括了常见的鼠标事件及绘图操作,需要的朋友可以参考下
    2014-09-09
  • C#中的队列Queue<T>与堆栈Stack<T>

    C#中的队列Queue<T>与堆栈Stack<T>

    这篇文章介绍了C#中的队列Queue<T>与堆栈Stack<T>,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • c#多进程通讯的实现示例

    c#多进程通讯的实现示例

    本文主要介绍了c#多进程通讯的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • c# 使用线程对串口serialPort进行收发数据(四种)

    c# 使用线程对串口serialPort进行收发数据(四种)

    本文主要介绍了c# 使用线程对串口serialPort进行收发数据,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • C# 中的??操作符浅谈

    C# 中的??操作符浅谈

    (??) 用于如果类不为空值时返回它自身,如果为空值则返回之后的操作
    2013-04-04
  • C#编写一个简单记事本功能

    C#编写一个简单记事本功能

    这篇文章主要为大家详细介绍了C#编写一个简单记事本功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-10-10
  • DataGridView实现点击列头升序和降序排序

    DataGridView实现点击列头升序和降序排序

    这篇文章介绍了DataGridView实现点击列头升序和降序排序的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • C#实现微信跳一跳小游戏的自动跳跃助手开发实战

    C#实现微信跳一跳小游戏的自动跳跃助手开发实战

    前段时间微信更新了新版本后,带来的一款H5小游戏“跳一跳”在各朋友圈里又火了起来,类似以前的“打飞机”游戏,这游戏玩法简单,但加上了积分排名功能后,却成了“装逼”的地方,于是很多人花钱花时间的刷积分抢排名
    2018-01-01
  • C#中利用代理实现观察者设计模式详解

    C#中利用代理实现观察者设计模式详解

    学习模式注重精髓而非模板,本文为了便于说明假定了三方并对三方功能进行了划分,实际应用并不拘泥于此。如果情况合适将数据(文档)类设计为单件模式也是一种很不错的选择
    2014-01-01
  • 关于C#程序优化的五十种方法

    关于C#程序优化的五十种方法

    这篇文章主要介绍了C#程序优化的五十个需要注意的地方,使用c#开发的朋友可以看下
    2013-09-09

最新评论