添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • :空间比较 ,但是读取速度
  • 引用结构体ref struct 的数据保存在 中,因此它的读写速度非常快,另一方面,栈中的数据销毁很快,而不是像托管堆一样,交给GC去回收

    在大多数情况下, 连续的内存操作比非连续性的内存操作要快特别是读操作, 多线程下连续内存的写由于CPU缓存伪共享问题性能反而可能下降

    高性能操作

    Span/ReadOnlySpan 和 Memory/ReadOnlyMemory:快速访问内存的方法,无需进行不必要的复制操作。

    Memory 是什么?
    它是一种可变大小、可读写的内存块,可以安全地暴露给用户代码进行操作。

  • Memory 是可变的,所以我们可以直接在内存中操作数据,而不需要进行额外的拷贝操作。例如,当你需要从一个字节数组中获取一个子数组时,传统做法可能需要先分配一个新的数组,并将原数组中的数据复制到新数组中,而使用Memory 则可以直接创建一个指向原数组的Memory 对象,并通过切片等操作来获取子数组,避免了不必要的内存分配和拷贝。
  • 使用Memory 还可以减少垃圾回收的压力,因为我们不需要创建新的对象来存储数据。
  • Memory 还可以与Span 和ReadOnlySpan 类型一起使用,这些类型可以方便地对数据进行访问和操作。
  • Memory<byte> emptyMemory = Memory<byte>.Empty; // 创建一个空内存块
    byte[] byteArray = new byte[10]; 
    Memory<byte> memory = byteArray; // 数组=>内存块
    byte[] array = memory.Span.ToArray(); // 内存块=>数组
    Memory<byte> slice = memory.Slice(2, 2); // 内存切片

    注意:当你将byte[]数组传递给Memory 的构造函数或者使用AsMemory()方法时,Memory 会引用 同一块内存 ,而不会进行内存复制。这意味着对Memory 的修改会直接反映在原始的byte[]数组上,反之亦然。

    byte[] byteArray = new byte[] { 0x12, 0x34, 0x56, 0x78, 0x9A };
    Memory<byte> byteMemory = byteArray.AsMemory();
    // 修改Memory<byte>
    byteMemory.Span[0] = 0xAB;
    Console.WriteLine(byteArray[0]);  // 输出:0xAB

    注意: Span<T> 就是利用 ref struct 的产物

    Span<T>中并不直接存储数据,它主要包含两个主要的信息:一个是指向数据的指针(或引用),另一个是数据的长度。这使得Span<T>能够表示一个连续的内存块,但并不实际拥有或复制这些数据。因此,当你创建一个Span<T>时,你实际上只是创建了一个轻量级的结构来引用现有的内存区域,而没有涉及任何数据的移动或复制。

    System.IO.Pipelines:用于高性能 I/O。

    System.IO.Pipelines 是一个用于读写数据流的高性能 API。它主要由三个部分组成: Pipe PipelineReader PipelineWriter

    Pipe 是一个 异步 线程安全 缓冲区 ,它让数据在生产者和消费者之间流动。 PipelineReader PipelineWriter 则是 Pipe 的读取和写入端点。

    有什么优点?

  • 高性能 :能够处理大量数据,而且不需要额外的内存分配,这意味着你可以减少内存使用量。
  • 低延迟 :它能够在不阻塞线程池中的线程的情况下处理数据,这意味着你的应用程序能够更快地响应请求。
  • 异步读写 :支持异步读写,这意味着你的应用程序能够同时处理多个请求,而不会阻塞线程池中的线程。
  • 可扩展性 :可以很容易地扩展到多个处理器,从而实现高并发处理。
  • 有哪些应用场景?

  • 网络编程: 它能够帮你高效地处理大量的网络数据流。你可以使用 PipelineWriter 将数据写入缓冲区,在另一个线程中使用 PipelineReader 读取缓冲区中的数据,并进行处理。
  • 文件处理: 你可以将文件分块读取到缓冲区中,然后使用 PipelineReader 读取缓冲区中的数据,并进行处理。这样可以大大减少内存分配和文件 I/O 的开销,从而提高文件处理的效率。
  • 怎么使用?

  • 创建Pipe :创建一个缓冲区,用于读取和写入数据。
  • 写入数据 :使用 PipelineWriter 将数据写入缓冲区。
  • 读取数据 :使用 PipelineReader 读取缓冲区中的数据,并进行处理。
  • 使用 ValueTask 代替 Task

    ValueTask :轻量级任务类型,表示可能异步完成的操作。

    实现异步迭代器时,C#编译器会利用此优势,以使异步迭代器尽可能免于额外内存分配。

    System.Buffers:该库提供了一组用于创建和管理缓冲区的类。

    这里面有些重点类, 只列出来, 后面单独讲解:

  • System.Buffers.ArrayPool<T> 数组池, 返回的是byte[]
  • System.Buffers.MemoryPool<T> 内存池, 返回的是IMemoryOwner
  • System.Buffers.IMemoryOwner<T> 内存池的返回类型, 其内部实现Memory
  • System.Buffers.IBufferWriter<T> 与其实现 ArrayBufferWriter<T> 这个不常用, 但是在一些高级类中会遇到, 通常用在复杂的业务场景中, 简单的业务场景直接使用Span 或Memory 即可
  • System.Buffers.ReadOnlySequence<T> 只读序列, 用于表示非连续内存序列的结构。
  • System.Buffers.ReadOnlySequenceSegment<T> 只读序列分段, 一个或多个分段可以组成一个ReadOnlySequence
  • System.Buffers.SequenceReader<T> 用于高效读取序列数据的工具, 它特别适用于处理 ReadOnlySequence 类型的数据
  • System.Buffers.SequenceReaderExtensions 是SequenceReader 类的扩展类, 提供了从二进制数据读取字节序特定数值的功能
  • System.Buffers.SearchValues<T> 对ReadOnlySpan 进行高效搜索,其内只有一个Contains(T)方法
  • 注意: ReadOnlySequence 相关类使用比较复杂, 用好了可以极大提高性能降低内存, 用不好就会适得其反.

    System.Buffers.ReadOnlySequence 只读序列,不连续的Memory

    内存片段 ReadOnlySequenceSegment 是 ReadOnlySequence 的基础。

    在我们读取数据的过程,很多时候会出现如下场景:不知道数据实际大小, 一次性申请大量内存开销太大, 此时我们往往会使用动态内存的方案,通过链表的方式串联起来,从而形成逻辑意义上的数据流。可以理解成 非连续内存的ReadOnlyMemory<T>组成的链表 , 我们在使用它时可以看成一个虚拟的ReadOnlyMemory

    其中 System.IO.Pipelines 它扮演了重要的角色.

    扩展阅读: https://www.cnblogs.com/jionsoft/p/13676277.html

    ArraySegment 数组段(分隔一维数组的一部分)

    它表示数组的一段 连续区域或片段 。当你需要处理数组的不同部分而 不复制整个数组 时,ArraySegment 非常有用。原始数组必须是 一维数组 ,并且必须具有从零开始的索引。

    总的来说,ArraySegment 更适合用于传统数组操作中的片段引用和修改,而 Span 则更适合用于高性能和内存安全的内存操作。在现代 C# 中,通常推荐使用Span 来代替ArraySegment ,以获得更好的性能和灵活性。但在一些高性能库中还是经常会看到ArraySegment

    ArraySegment<T>在做数组切片时不会发生内存复制,且其可以很方便的转化成ReadOnlySpan<T>, 但是其不能直接转化为Array如果要转化会发生内存复制,因为Array内存结构中有一个数组长度,如果要不发生内存复制的情况下转化那么必须要破坏源数组结构

    System.Numerics大型整数的类

    该库提供了一组用于处理大型整数的类。

    System.Collections.Frozen

    不可变只读集合,它的创建成本相对较高,但提供出色的 查找性能 , 非常适用于配置类。其不同于 readonly 描述符, readonly创建后可以更改key/value,只是不能重新赋值(=), 但是frozen创建后key/value也不能修改.

    Pool池类

    System.Buffers.ArrayPool

  • 需要频繁分配和释放 小型内存块 的情况,例如在高性能计算、并行处理或缓存中使用。
  • 对内存使用量敏感的情况,例如在资源受限的环境中使用。
  • System.Buffers.MemoryPool

    (注意:内存池中 无Return方法 ,GC会高效回收, 池子本质申请的是Memory
    需使用 using() 自动释放,或者 Dispose() 手动释放

  • 需要动态分配 较大内存块 的情况,例如处理大型文件、网络数据流等。
  • 长时间持有 内存块的情况,例如在长时间运行的服务中使用。
  • 需要更 高级别的内存管理控制 也就是 Memory<T> ,例如自定义的内存管理策略。
  • Microsoft.Extensions.ObjectPool.ObjectPool

    对象池, 使用对象池在返回池时需要进行初始化

    System.Threading.ThreadPool / Task

    使用 ref struct 做到 0 GC

    C# 7 开始引入了一种叫做 ref struct 的结构,这种结构本质是 struct ,结构存储在 栈内存 。但是与 struct 不同的是,该结构 不允许实现任何接口 ,并由编译器保证该结构 永远不会被装箱 ,因此不会给 GC 带来任何的压力。其中 Span<T> 就是利用 ref struct 的产物

    使用 unsafe

    代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用 指针变量 。不安全代码或非托管代码是指使用了指针变量的代码块。

    使用 stackalloc 在 上分配连续内存

    对于部分性能敏感却需要使用少量的 连续内存 的情况,不必使用数组,而可以通过 stackalloc 直接在 上分配内存,并使用 Span<T> 来安全的访问,同样的,这么做可以做到 0 GC 压力。

    连续内存 的数据结构有如下: 数组/字符串/结构体/枚举

    使用不可变类型

    不可变类型 是一类特殊的类型,它们被设计为创建后就不能被修改。优点:线程安全/缓存和复用/易于维护/函数式编程, 特别适用于多线程和分布式系统等需要高度并发和异步编程的场景。

    有如下一些操作使用不可变数据

  • 使用 in关键字 传递不可修改的引用给方法
  • 使用 record类型 和不可变数据
  • 使用 readonly类型
  • 使用不可变类型, 例如:字符串/元组/值类型/只读属性/不可变集合类(ReadOnlyDictionary/FrozenDictionary等)
  • BinaryPrimitives 细粒度操作字节数组

    System.Buffers.Binary.BinaryPrimitives 命名空间 BinaryPrimitives 的实现原理是 BitConverter ,BinaryPrimitives 对 BitConverter 做了一些封装。BinaryPrimitives 中有 大端小端 之分, 在向byte[]中写入和读取 基本类型 时经常用到此类

    MemoryMarshal可以将一种结构转换为另一种结构

    MemoryMarshal 提供与 Memory 、ReadOnlyMemory 、Span 和 ReadOnlySpan 进行交互操作的方法。最简单的说法是,MemoryMarshal 可以将一种结构转换为另一种结构。

    var byteArray = new byte[] { 1, 0, 0, 0, 2, 0, 0, 0 };
    Span<byte> byteSpan = byteArray.AsSpan();
    Span<int> intSpan = MemoryMarshal.Cast<byte, int>(byteSpan); // int [] {1,2}

    System.Runtime.Caching.MemoryCache

    缓存类, 类似memcache/redis

    RecyclableMemoryStream 代替 MemoryStream

    注意: 在处理大的流时采用RecyclableMemoryStream, 在处理小的流时采用MemoryStream。因为测试发现用于处理小的流(百个字节以内)时RecyclableMemoryStream比MemoryStream慢几倍, 内存消耗也相差不大。 但是处理几万字节流时, 其内存消耗比MemoryStream少百倍, 其getSpan等的耗时比MemoryStream少几十倍。

    RecyclableMemoryStream 通过池化MemoryStream底层buffer来降低内存占用率、GC暂停时间和GC次数达到提升性能目的, 开始Stream会写入 小型池 ,当小型池装不下时会复制到 大型池 。这个库相比其他Stream的使用方法有所不同, 主要表现如下:

  • RecyclableMemoryStreamManager 用于管理 RecyclableMemoryStream 的池等
  • 最好不要调用 toArray() 会重新 new byte[] 分配内存
  • 最好少调用 GetBuffer() TryGetBuffer() 因为它会将 小型池 内容复制到 大型池 ,会发生一次内存复制
  • 最好少调用 Write() 因为大量调用会导致 小型池 大量的内存片段, 最好一个Stream只调用几个, 千万不要在循环中调用, 否则再调用GetBuffer就会将小型池的内存片段复制到大型池再返回, 会有大量性能损失
  • 写入时调用 GetWritableBuffer() , GetSpan() , GetMemory() 获取一个buffer的缓冲 引用 , 然后在内存引用中添加/修改, 之后再调用 Advance() 将缓冲区数据写入到小型池/大型池/缓冲区
  • 读取时调用 GetReadOnlySequence() 可以以内存片段形式返回, 不会发生任何内存复制
  • private static readonly RecyclableMemoryStreamManager manager = new RecyclableMemoryStreamManager();
    using var memoryStream = manager.GetStream();
    # 不推荐的写法
    # 注意, 这样大量调用 Write(1) 会出现性能问题, 所以, 其最好不要和系统内置的 binaryReader 等库操作.
    using BinaryReader binaryReader = new BinaryReader(memoryStream);
    for (int i = 0; i < 1024; i++)
        binaryReader.Write(1);
    memoryStream.toArray();
    # 推荐写法
    // 获取缓冲区(具体是获取小型池/大型池/还是缓冲区的块, 取决于数据量大小)
    Span<byte> buffs = memoryStream.GetSpan(1024);
    // 写入&修改缓冲区
    for (int i = 0; i < 1024; i++)
        buffs[i] = 1;
    // 缓冲区写入小型池(小数据)/大型池(启用大型池时)/缓冲区(大于定义的块时)
    // 如果是 小型池/大型池 调用不需要复制内存因为Span就已经直接修改了, 如果是 缓冲区 有一次内存复制但不会产生gc
    memoryStream.Advance(1024)

    异步编程可以让你的应用程序在等待某些操作(如I/O操作)完成时不会阻塞主线程,从而提高应用程序的性能和响应性。使用async和await关键字可以简化异步编程。

    细节优化技巧

  • 使用简单的数据结构:尽量使用简单的数据结构,如数组而不是列表(List)、结构体而不是类等。
  • 避免装箱和拆箱:尽量避免值类型和引用类型之间的转换,因为这会引入额外的开销。
  • 使用不可变类型:不可变类型可以减少并发环境下的竞争和复杂性,提高性能。
  • 使用内存池:适当地重用对象,可以通过内存池来避免频繁地分配和回收内存。
  • 避免使用反射:反射操作会比直接调用方法或访问属性慢很多,尽量避免在性能敏感的代码路径中使用反射。
  • 使用并行编程:在需要处理大量数据或进行并行计算时,可以考虑使用并行编程库,如 Parallel 类或异步编程模式。
  • 优化关键路径:对于性能关键的代码路径,进行适当的优化,比如减少循环内部的计算、减少内存分配等。
  • 使用性能分析工具:利用性能分析工具(如 Profiler)来找出性能瓶颈,并有针对性地进行优化。
  • 使用StringBuilder而不是String:在C#中,字符串是不可变的。这意味着每次对字符串进行修改时,都会生成一个新的字符串对象。这可能会导致大量不必要的内存分配和垃圾回收。对于需要多次修改字符串的情况,最好使用StringBuilder类,这是一个可变对象,可以更有效地处理字符串。
  • 使用异步编程:异步编程可以让您在等待某些操作(如I/O操作)完成时,同时执行其他操作。在C#中,async和await关键字是处理异步操作的常用方式。
  • 合适的数据结构和算法:选择正确的数据结构和算法可以极大地提高代码的性能。例如,如果您需要频繁地在列表中查找元素,那么可能应该使用HashSet而不是List。
  • .NET 5引入了AOT,.NET Native是一个AOT编译器,通过预先将.NET应用程序编译为本地机器代码,加快了应用程序的启动时间和执行效率。
  • PGO (Profile-Guided Optimization) 是使用配置文件引导的优化,是一种编译器优化技术,借助配置文件来引导编译,达到提高程序运行时性能的目的。PGO通过收集运行时信息来指导JIT如何优化代码,相比以前没有PGO时可以做更多以前难以完成的优化。这项优化是一个通用技术,不局限于某一门语言。
  • 【译】ASP.NET Core 6 中的性能改进
    ASP.NET Core 7 中的性能改进
    【译】.NET 7 中的性能改进
    .NET高性能编程Span/Memory
    在 C# 中使用 Span 和 Memory 编写高性能代码
    System.IO.Pipelines: C#高性能IO
    C# 使用Pipelines处理Socket数据包
    github.com/kk-cpp/KeenNet
    编写高效的代码,你应该了解Array、Memory、ReadOnlySequence