.NET性能优化-使用RecyclableMemoryStream替代MemoryStream

性能,NET,MemoryStream,优化 · 浏览次数 : 2185

小编点评

**性能提升方法:** - 使用**RecyclableMemoryStream**替代**MemoryStream**。 - 设置**MaximumFreeLargePoolBytes**和**MaximumFreeSmallPoolBytes**属性以控制池中的最大占用空间。 - 设置**bufferMultiple**和**maxBufferSize**以设置每个流的缓存大小。 - 使用**GetReadOnlySequence**等方法读取流,避免内存分配。 - 在配置中考虑**bufferMultiple**、**maxBufferSize**和**MaximumFreeLargePoolBytes**。 **其他优化技巧:** - 使用性能分析工具(如**APM**)识别性能瓶颈。 - 优化数据库查询和数据访问。 - 使用异步编程技术处理大量任务。 - 使用**IBufferWriter**避免频繁内存分配。 - 使用**CopyTo**和**WriteTo**方法减少内存复制。 **参考资料:** - **Recyclable MemoryStream 库** - **.NET 性能优化交流群**

正文

提到MemoryStream大家可能都不陌生,在编写代码中或多或少有使用过;比如Json序列化反序列化、导出PDF/Excel/Word、进行图片或者文字处理等场景。但是如果使用它高频、大数据量处理这些数据,就存在一些性能陷阱。

今天给大家带来的这个优化技巧其实就是池化MemoryStream的版本RecyclableMemoryStream,它通过池化MemoryStream底层buffer来降低内存占用率、GC暂停时间和GC次数达到提升性能目的。

它的开源库地址如下链接:

https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream

使用它也非常简单,直接安装对应的Nuget包即可,目前最新版本是2.2.1版本。

// 命令行安装
dotnet add package Microsoft.IO.RecyclableMemoryStream --version 2.2.1
// csproj 安装
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />

然后创建一个RecyclableMemoryStreamManager对象,即可使用它的GetStream方法来获取一个池化的流,当然使用完这个流以后需要调用Dispose方法将其归还到池中,也可以使用using模式来释放。

class Program
{
    private static readonly RecyclableMemoryStreamManager manager = new RecyclableMemoryStreamManager();

    static void Main(string[] args)
    {
        var sourceBuffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
        
        using (var stream = manager.GetStream())
        {
            stream.Write(sourceBuffer, 0, sourceBuffer.Length);
        }
    }
}

在创建RecyclableMemoryStreamManagerGetStream时有很多选项,可以设置底层buffer的大小、为流进行命名隔离等精细化的选项,这些大家可以看官方文档了解,本文不再赘述。

性能比较

为了直观的比较性能,我构建了一个Benchmark,这个基准测试分别使用MemoryStreamRecyclableMemoryStream实现数据缓冲的功能,下面是测试代码:

public class BenchmarkRecyclableMemoryStream
{
    // 生成随机数
    private static readonly Random Random = new(1024);

    // 填充的数据
    private static readonly byte[] Data = Enumerable.Range(0, 81920).Select(d => (byte) d).ToArray();
    // 每次随机填充
    private static readonly int[] DataLength = Enumerable.Range(0, 1000).Select(d => Random.Next(10240, 81920)).ToArray();

    // RecyclableManager
    private static readonly RecyclableMemoryStreamManager Manager = new();
    
    [Benchmark(Baseline = true)]
    public long UseMemoryStream()
    {
        var sum = 0L; 
        for (int i = 0; i < DataLength.Length; i++)
        {
            using var stream = new MemoryStream();
            stream.Write(Data, 0, DataLength[i]);
            sum += stream.Length;
        }

        return sum;
    }

    [Benchmark]
    public long UseRecyclableMemoryStream()
    {
        var sum = 0L; 
        for (int i = 0; i < DataLength.Length; i++)
        {
            using var stream = Manager.GetStream();
            stream.Write(Data, 0, DataLength[i]);
            sum += stream.Length;
        }

        return sum;
    }
}

下方是测试的结果,可以看到使用RecyclableMemoryStream比直接使用MemoryStream在内存和速度上有很大的优势。

  • 执行效率快51%
  • 内存分配要低99.4%

工作原理

RecyclableMemoryStream提升GC性能的方式是通过将缓冲区分配和保持在第二代堆,这能减少FullGC的频率,另外如果您设置的缓冲区大小超过85,000字节,那么缓冲区将分配在LOH上,GC不会经常扫描这些对象堆。

RecyclableMemoryStreamManager类维护了两个独立的对象池:

  • 小型池:保存小型缓冲区(可配置大小),默认情况下用于所有正常的读、写操作,多个小的缓冲区能链接在一起,形成单独的Stream
  • 大型池:保存大型缓冲区,只有在必须需要单个且连续缓冲区才使用,比如调用GetBuffer方法,它可以创建比单个缓冲区大的多的Stream,最大不超过.NET对数组类型的限制。

RecyclableMemoryStream首先会使用一个小的缓冲区,随着写入数据的增多,会将其它缓冲区链接起来组合使用。如果您调用了GetBuffer方法,并且已有的数据大于单个小缓冲区的容量,那么就会被转换为大缓冲区。

另外您还可以为Stream设置初始容量,如果容量大于单个缓冲区大小,会在一开始就链接好多个块,当然也可以直接分配大型缓冲区,只需将asContiguousBuffer设置为true。

大型池有两个版本:

  • 线性(默认):指定一个倍数和最大的大小,然后创建一个缓冲区数组,从(1x倍数)、(2x倍数)一直到最大值。
  • 指数:缓冲区不是线性增长而是指数增长,每个槽大小将增加一倍。

如下图所示:

那么您应该用哪一个?这取决于您的业务场景。如果您的缓冲区大小不可预测,那么线性缓冲区可能更合适。如果您知道不可能分配较长的流长度,但是可能有很多较小尺寸的流,那么选择指数版本可能会导致较少的总体内存使用。

缓冲区是在第一次被请求时按需创建的。使用完Stream后,这些缓冲区将通过RecyclableMemoryStreamDispose方法返回到池中。当这种返回发生时,RecyclableMemoryStreamManager将使用属性MaximumFreeSmallPoolBytesMaximumFreeLargePoolBytes来决定是否将这些缓冲区放回池中,或者让它们离开(从而被垃圾收集)。正是通过这些属性,你决定了你的池子可以增长到多大。如果你把这些属性设置为0,你就会有无限制的池增长,这与内存泄漏基本上没有区别。对于每一个应用程序,你必须通过分析和实验来确定内存池大小和垃圾收集之间的适当平衡。

如果忘记调用流的 Dispose 方法,可能会导致内存泄漏。为了帮助您避免这种情况,每个流都有一个终结器,一旦没有更多对流的引用,CLR 将调用该终结器。此终结器将引发有关泄漏流的事件或记录有关泄漏流的消息。

请注意,由于性能原因,缓冲区从来没有预先初始化或归零。您有责任确保它们的内容是有效和安全的,可以使用缓冲区回收。

使用指南

虽然这个库力求非常通用化,并且不会对如何使用它施加太多限制,但是它的目的是减少由于频繁的大量分配而产生的垃圾收集的成本。因此,以下是一些对你有用的通用使用指南:

  1. blockSizelargeBufferMultiplemaxBufferSizeMaximumFreeLargePoolBytes MaximumFreeSmallPoolBytes属性设置为符合你的应用和资源要求的合理值。如果你不设置MaximumFreeLargePoolBytesMaximumFreeSmallPoolBytes,就有可能出现无限制的内存增长!
  2. 每个流总是被精确地Dispose一次。
  3. 大多数应用程序不应该调用ToArray,如果可能,应该避免调用GetBuffer。相反,使用GetReadOnlySequence来读取,使用IBufferWriter方法GetSpanGetMemoryAdvance来写入。还有一些杂七杂八的CopyToWriteTo方法,可能很方便。重点是要尽可能避免产生不必要的GC压力。
  4. 通过实验找到适合你情况的设置。

在你尝试用这个库来优化你的方案之前,对垃圾收集器有一定的了解是一个非常好的主意。像垃圾收集这样的文章,或者像《编写高性能的.NET代码》这样的书,将帮助你理解这个库的设计原则。

在配置选项时,要考虑这样的问题。

  • 我期望的流的长度分布是怎样的?
  • 有多少个流会在同一时间被使用?
  • GetBuffer是否经常被调用?我需要多大程度的使用大型池缓冲区?
  • 我需要对活动高峰有多大的弹性? 即我应该保留多少空闲字节以备不时之需?
  • 我在要使用的机器上有哪些物理内存限制?

总结

本文中介绍了一个通用的MemoryStream池化库,使用它能显著的提升你系统的性能,你几乎可以在任何场景使用RecyclableMemoryStream替代MemoryStream。要知道在我们性能评测中,RecyclableMemoryStreamMemoryStream快51%,而且它能节省99.4%的内存分配。

.NET性能优化交流群

相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群,但是由于各种原因一直都没创建,现在很高兴的在这里宣布,我创建了一个专门交流.NET性能优化经验的群组,主题包括但不限于:

  • 如何找到.NET性能瓶颈,如使用APM、dotnet tools等工具
  • .NET框架底层原理的实现,如垃圾回收器、JIT等等
  • 如何编写高性能的.NET代码,哪些地方存在性能陷阱

希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能问题和宝贵的性能分析优化经验。由于已经达到200人,可以加我微信,我拉你进群: ls1075

与.NET性能优化-使用RecyclableMemoryStream替代MemoryStream相似的内容:

.NET性能优化-使用RecyclableMemoryStream替代MemoryStream

提到MemoryStream大家可能都不陌生,在编写代码中或多或少有使用过;比如Json序列化反序列化、导出PDF/Excel/Word、进行图片或者文字处理等场景。但是如果使用它高频、大数据量处理这些数据,就存在一些性能陷阱。 今天给大家带来的这个优化技巧其实就是池化MemoryStream的版本

.NET性能优化-使用内存+磁盘混合缓存

我们回顾一下上一篇文章中的内容,有一个朋友问我这样一个问题: > 我的业务依赖一些数据,因为数据库访问慢,我把它放在Redis里面,不过还是太慢了,有什么其它的方案吗? 其实这个问题比较简单的是吧?Redis其实属于网络存储,我对照下面的这个表格,可以很容易的得出结论,既然网络存储的速度慢,那我们就

.NET性能优化-复用StringBuilder

在之前的文章中,我们介绍了dotnet在字符串拼接时可以使用的一些性能优化技巧。比如: 为StringBuilder设置Buffer初始大小 使用ValueStringBuilder等等 不过这些都多多少少有一些局限性,比如StringBuilder还是会存在new StringBuilder()这

.NET性能优化-ArrayPool同时复用数组和对象

前两天在微信后台收到了读者的私信,问了一个这样的问题,由于私信回复有字数和篇幅限制,我在这里统一回复一下。读者的问题是这样的: 大佬您好,之前读了您的文章受益匪浅,我们有一个项目经常占用 7-8GB 的内存,使用了您推荐的ArrayPool以后降低到 4GB 左右,我还想着能不能继续优化,于是 du

[转帖]腾讯服务注册中心演进及性能优化实践

https://my.oschina.net/u/4587289/blog/5577840 导语 注册中心作为微服务架构的核心,承担服务调用过程中的服务注册与寻址的职责。注册中心的演进是随着业务架构和需求的发展而进行演进的。腾讯当前内部服务数超百万级,日调用量超过万亿次,使用着统一的注册中心 ——

使用.NET7和C#11打造最快的序列化程序-以MemoryPack为例

## 译者注 本文是一篇不可多得的好文,MemoryPack 的作者 neuecc 大佬通过本文解释了他是如何将序列化程序性能提升到极致的;其中从很多方面(可变长度、字符串、集合等)解释了一些性能优化的技巧,值得每一个开发人员学习,特别是框架的开发人员的学习,一定能让大家获益匪浅。 ## 简介 我发

.NET周报【12月第1期 2022-12-08】

国内文章 CAP 7.0 版本发布通告 - 支持延迟消息,性能炸了? https://www.cnblogs.com/savorboard/p/cap-7-0.html) 今天,我们很高兴宣布 CAP 发布 7.0 版本正式版,我们在这个版本中带来了大批新特性以及对性能的优化和改进。 使用.NET7

[转帖]【mmap】深度分析mmap:是什么 为什么 怎么用 性能总结

`https://blog.csdn.net/bandaoyu/article/details/106750990` 目录 有什么用? 1、文件映射 2、分配内存(匿名文件映射) mmap基础概念 mmap内存映射原理 mmap和常规文件操作的区别 mmap优点总结 mmap相关函数 mmap使用细

.NET性能优化-是时候换个序列化协议了

计算机单机性能一直受到摩尔定律的约束,随着移动互联网的兴趣,单机性能不足的瓶颈越来越明显,制约着整个行业的发展。不过我们虽然不能无止境的纵向扩容系统,但是我们可以分布式、横向的扩容系统,这听起来非常的美好,不过也带来了今天要说明的问题,分布式的节点越多,通信产生的成本就越大。 网络传输带宽变得越来越

.Net核心级的性能优化(GC篇)

1.前言 大部分人对于.Net性能优化,都停留在业务层面。或者简单的.Net框架配置层面。本篇来看下.Net核心部分GC垃圾回收配置:保留VM,大对象,独立GC,节省内存等.Net8里面有很多的各种GC配置,用以帮助你的程序进行最大程度性能提升和优化。 文章分为两部分,第一个是GC有哪些动作可以性能