堆
大
慢
引用结构体ref struct 的数据保存在 栈 中,因此它的读写速度非常快,另一方面,栈中的数据销毁很快,而不是像托管堆一样,交给GC去回收
引用结构体ref struct
栈
在大多数情况下, 连续的内存操作比非连续性的内存操作要快特别是读操作, 多线程下连续内存的写由于CPU缓存伪共享问题性能反而可能下降 。
连续的内存操作比非连续性的内存操作要快特别是读操作, 多线程下连续内存的写由于CPU缓存伪共享问题性能反而可能下降
Memory 是什么? 它是一种可变大小、可读写的内存块,可以安全地暴露给用户代码进行操作。
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>
ref struct
Span<T>中并不直接存储数据,它主要包含两个主要的信息:一个是指向数据的指针(或引用),另一个是数据的长度。这使得Span<T>能够表示一个连续的内存块,但并不实际拥有或复制这些数据。因此,当你创建一个Span<T>时,你实际上只是创建了一个轻量级的结构来引用现有的内存区域,而没有涉及任何数据的移动或复制。
System.IO.Pipelines 是一个用于读写数据流的高性能 API。它主要由三个部分组成: Pipe 、 PipelineReader 和 PipelineWriter 。
System.IO.Pipelines
Pipe
PipelineReader
PipelineWriter
Pipe 是一个 异步 、 线程安全 的 缓冲区 ,它让数据在生产者和消费者之间流动。 PipelineReader 和 PipelineWriter 则是 Pipe 的读取和写入端点。
异步
线程安全
缓冲区
有什么优点?
高性能
低延迟
异步读写
可扩展性
有哪些应用场景?
网络编程:
文件处理:
怎么使用?
创建Pipe
写入数据
读取数据
ValueTask :轻量级任务类型,表示可能异步完成的操作。
实现异步迭代器时,C#编译器会利用此优势,以使异步迭代器尽可能免于额外内存分配。
这里面有些重点类, 只列出来, 后面单独讲解:
System.Buffers.ArrayPool<T>
System.Buffers.MemoryPool<T>
System.Buffers.IMemoryOwner<T>
System.Buffers.IBufferWriter<T>
ArrayBufferWriter<T>
System.Buffers.ReadOnlySequence<T>
System.Buffers.ReadOnlySequenceSegment<T>
System.Buffers.SequenceReader<T>
System.Buffers.SequenceReaderExtensions
System.Buffers.SearchValues<T>
注意: ReadOnlySequence 相关类使用比较复杂, 用好了可以极大提高性能降低内存, 用不好就会适得其反.
内存片段 ReadOnlySequenceSegment 是 ReadOnlySequence 的基础。
在我们读取数据的过程,很多时候会出现如下场景:不知道数据实际大小, 一次性申请大量内存开销太大, 此时我们往往会使用动态内存的方案,通过链表的方式串联起来,从而形成逻辑意义上的数据流。可以理解成 非连续内存的ReadOnlyMemory<T>组成的链表 , 我们在使用它时可以看成一个虚拟的ReadOnlyMemory
非连续内存的ReadOnlyMemory<T>组成的链表
其中 System.IO.Pipelines 它扮演了重要的角色.
扩展阅读: https://www.cnblogs.com/jionsoft/p/13676277.html
它表示数组的一段 连续区域或片段 。当你需要处理数组的不同部分而 不复制整个数组 时,ArraySegment 非常有用。原始数组必须是 一维数组 ,并且必须具有从零开始的索引。
连续区域或片段
不复制整个数组
一维数组
总的来说,ArraySegment 更适合用于传统数组操作中的片段引用和修改,而 Span 则更适合用于高性能和内存安全的内存操作。在现代 C# 中,通常推荐使用Span 来代替ArraySegment ,以获得更好的性能和灵活性。但在一些高性能库中还是经常会看到ArraySegment
ArraySegment<T>在做数组切片时不会发生内存复制,且其可以很方便的转化成ReadOnlySpan<T>, 但是其不能直接转化为Array如果要转化会发生内存复制,因为Array内存结构中有一个数组长度,如果要不发生内存复制的情况下转化那么必须要破坏源数组结构
该库提供了一组用于处理大型整数的类。
不可变只读集合,它的创建成本相对较高,但提供出色的 查找性能 , 非常适用于配置类。其不同于 readonly 描述符, readonly创建后可以更改key/value,只是不能重新赋值(=), 但是frozen创建后key/value也不能修改.
查找性能
readonly
小型内存块
(注意:内存池中 无Return方法 ,GC会高效回收, 池子本质申请的是Memory ) 需使用 using() 自动释放,或者 Dispose() 手动释放
无Return方法
using()
Dispose()
较大内存块
长时间持有
高级别的内存管理控制
Memory<T>
对象池, 使用对象池在返回池时需要进行初始化
C# 7 开始引入了一种叫做 ref struct 的结构,这种结构本质是 struct ,结构存储在 栈内存 。但是与 struct 不同的是,该结构 不允许实现任何接口 ,并由编译器保证该结构 永远不会被装箱 ,因此不会给 GC 带来任何的压力。其中 Span<T> 就是利用 ref struct 的产物
struct
栈内存
不允许实现任何接口
永远不会被装箱
代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用 指针变量 。不安全代码或非托管代码是指使用了指针变量的代码块。
unsafe
指针变量
对于部分性能敏感却需要使用少量的 连续内存 的情况,不必使用数组,而可以通过 stackalloc 直接在 栈 上分配内存,并使用 Span<T> 来安全的访问,同样的,这么做可以做到 0 GC 压力。
连续内存
stackalloc
连续内存 的数据结构有如下: 数组/字符串/结构体/枚举
不可变类型 是一类特殊的类型,它们被设计为创建后就不能被修改。优点:线程安全/缓存和复用/易于维护/函数式编程, 特别适用于多线程和分布式系统等需要高度并发和异步编程的场景。
不可变类型
有如下一些操作使用不可变数据
in关键字
record类型
readonly类型
System.Buffers.Binary.BinaryPrimitives 命名空间 BinaryPrimitives 的实现原理是 BitConverter ,BinaryPrimitives 对 BitConverter 做了一些封装。BinaryPrimitives 中有 大端小端 之分, 在向byte[]中写入和读取 基本类型 时经常用到此类 。
System.Buffers.Binary.BinaryPrimitives
BinaryPrimitives
BitConverter
大端小端
在向byte[]中写入和读取 基本类型 时经常用到此类
MemoryMarshal 提供与 Memory 、ReadOnlyMemory 、Span 和 ReadOnlySpan 进行交互操作的方法。最简单的说法是,MemoryMarshal 可以将一种结构转换为另一种结构。
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}
缓存类, 类似memcache/redis
注意: 在处理大的流时采用RecyclableMemoryStream, 在处理小的流时采用MemoryStream。因为测试发现用于处理小的流(百个字节以内)时RecyclableMemoryStream比MemoryStream慢几倍, 内存消耗也相差不大。 但是处理几万字节流时, 其内存消耗比MemoryStream少百倍, 其getSpan等的耗时比MemoryStream少几十倍。
RecyclableMemoryStream 通过池化MemoryStream底层buffer来降低内存占用率、GC暂停时间和GC次数达到提升性能目的, 开始Stream会写入 小型池 ,当小型池装不下时会复制到 大型池 。这个库相比其他Stream的使用方法有所不同, 主要表现如下:
RecyclableMemoryStream
小型池
大型池
RecyclableMemoryStreamManager
toArray()
new byte[]
GetBuffer()
TryGetBuffer()
Write()
GetWritableBuffer()
GetSpan()
GetMemory()
引用
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关键字可以简化异步编程。
【译】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
最后更新于 2024-05-11 17:45:49 并被添加「」标签,已有 335 位童鞋阅读过。 本站使用「 署名 4.0 国际 」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处