当前位置: 代码网 > it编程>编程语言>Asp.net > C#中的高性能内存操作的利器:Span<T>和Memory<T>

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

2025年08月02日 Asp.net 我要评论
在.net开发中,内存管理一直是影响性能的关键因素。传统的字符串处理、数组操作等往往伴随着大量的内存分配和复制操作,这些不必要的开销在高性能场景下尤为明显。为了解决这个问题,.net core 2.1

在.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#代码,特别是在处理大数据量、高性能要求的应用场景中。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com