开发者

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

目录
  • Span:栈上分配的高性能利器
    • Span的本质
    • Span与字符串处理
    • 使用stackalloc与Span
    • Span的关键特性
  • Memory:异步操作的理想选择
    • Memory的定位
    • Memory与异步文件操作
    • Memory的关键特性
  • Span与Memory的对比选择
    • 实战应用场景
      • 高性能字符串解析
      • 二进制数据处理
    • 使用注意事项
      • 安全使用Span的建议
      • 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]);

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

        注意:数组堆上分配的引用类型,与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()}");
            }
        }

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

        使用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();
                }
            }
        }

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

        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]);

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

        Memory与异步文件操作

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

        using System.Runtime.CompilerServices;
        using System.Runtime.InteropServices;
        
        namespace AppSpanMemory
        {
            internal class Program
            {
                static asjavascriptync 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($"读取了 {bytesReaxqcpVd} 字节的数据");
                }
        
                static void ProcessData(Span<byte> data)
                {
                    Console.WriteLine($"前10个字节: {BitConverter.ToString(data.Slice(0, Math.Min(10, data.Length)).ToArray())}");
                }
            }
        }

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

        Memory的关键特性

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

        Span与Mhttp://www.devze.comemory的对比选择

        特性

        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()}'");
                }
        
            }
        }

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

        二进制数据处理

        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';
          www.devze.com          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())}");
                }
        
            }
        }

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

        使用注意事项

        安全使用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

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜