.NET性能优化-使用RecyclableMemoryStream替代MemoryStream
提到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);
}
}
}
在创建RecyclableMemoryStreamManager
和GetStream
时有很多选项,可以设置底层buffer的大小、为流进行命名隔离等精细化的选项,这些大家可以看官方文档了解,本文不再赘述。
性能比较
为了直观的比较性能,我构建了一个Benchmark,这个基准测试分别使用MemoryStream
和RecyclableMemoryStream
实现数据缓冲的功能,下面是测试代码:
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
后,这些缓冲区将通过RecyclableMemoryStream
的Dispose
方法返回到池中。当这种返回发生时,RecyclableMemoryStreamManager
将使用属性MaximumFreeSmallPoolBytes
和MaximumFreeLargePoolBytes
来决定是否将这些缓冲区放回池中,或者让它们离开(从而被垃圾收集)。正是通过这些属性,你决定了你的池子可以增长到多大。如果你把这些属性设置为0,你就会有无限制的池增长,这与内存泄漏基本上没有区别。对于每一个应用程序,你必须通过分析和实验来确定内存池大小和垃圾收集之间的适当平衡。
如果忘记调用流的 Dispose 方法,可能会导致内存泄漏。为了帮助您避免这种情况,每个流都有一个终结器,一旦没有更多对流的引用,CLR 将调用该终结器。此终结器将引发有关泄漏流的事件或记录有关泄漏流的消息。
请注意,由于性能原因,缓冲区从来没有预先初始化或归零。您有责任确保它们的内容是有效和安全的,可以使用缓冲区回收。
使用指南
虽然这个库力求非常通用化,并且不会对如何使用它施加太多限制,但是它的目的是减少由于频繁的大量分配而产生的垃圾收集的成本。因此,以下是一些对你有用的通用使用指南:
- 将
blockSize
、largeBufferMultiple
、maxBufferSize
、MaximumFreeLargePoolBytes
和MaximumFreeSmallPoolBytes
属性设置为符合你的应用和资源要求的合理值。如果你不设置MaximumFreeLargePoolBytes
和MaximumFreeSmallPoolBytes
,就有可能出现无限制的内存增长! - 每个流总是被精确地
Dispose
一次。 - 大多数应用程序不应该调用
ToArray
,如果可能,应该避免调用GetBuffer
。相反,使用GetReadOnlySequence
来读取,使用IBufferWriter
方法GetSpan
、GetMemory
和Advance
来写入。还有一些杂七杂八的CopyTo
和WriteTo
方法,可能很方便。重点是要尽可能避免产生不必要的GC压力。 - 通过实验找到适合你情况的设置。
在你尝试用这个库来优化你的方案之前,对垃圾收集器有一定的了解是一个非常好的主意。像垃圾收集这样的文章,或者像《编写高性能的.NET代码》这样的书,将帮助你理解这个库的设计原则。
在配置选项时,要考虑这样的问题。
- 我期望的流的长度分布是怎样的?
- 有多少个流会在同一时间被使用?
GetBuffer
是否经常被调用?我需要多大程度的使用大型池缓冲区?- 我需要对活动高峰有多大的弹性? 即我应该保留多少空闲字节以备不时之需?
- 我在要使用的机器上有哪些物理内存限制?
总结
本文中介绍了一个通用的MemoryStream
池化库,使用它能显著的提升你系统的性能,你几乎可以在任何场景使用RecyclableMemoryStream
替代MemoryStream
。要知道在我们性能评测中,RecyclableMemoryStream
比MemoryStream
快51%,而且它能节省99.4%的内存分配。
.NET性能优化交流群
相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群,但是由于各种原因一直都没创建,现在很高兴的在这里宣布,我创建了一个专门交流.NET性能优化经验的群组,主题包括但不限于:
- 如何找到.NET性能瓶颈,如使用APM、dotnet tools等工具
- .NET框架底层原理的实现,如垃圾回收器、JIT等等
- 如何编写高性能的.NET代码,哪些地方存在性能陷阱
希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能问题和宝贵的性能分析优化经验。由于已经达到200人,可以加我微信,我拉你进群: ls1075
.NET性能优化-使用RecyclableMemoryStream替代MemoryStream的更多相关文章
- 【腾讯Bugly干货分享】Android性能优化典范——第6季
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/580d91208d80e49771f0a07c 导语 这里是Android性能优 ...
- Unity性能优化(3)-官方教程Optimizing garbage collection in Unity games翻译
本文是Unity官方教程,性能优化系列的第三篇<Optimizing garbage collection in Unity games>的翻译. 相关文章: Unity性能优化(1)-官 ...
- 分享10条PHP性能优化的小技巧,帮助你更好的用PHP开发:
1. foreach效率更高,尽量用foreach代替while和for循环. 2. 循环内部不要声明变量,尤其是对象这样的变量. 3. 在多重嵌套循环中,如有可能,应当将最长的循环放在内层,最短循环 ...
- 阿里无线前端性能优化指南 (Pt.1 加载优化)
前言 阿里无线前端团队在过去一年对所负责业务进行了全面的性能优化.以下是我们根据实际经验总结的优化指南,希望对大家有所帮助. 第一部分仅包括数据加载期优化. 图片控制 对于网页特别是电商类页面来说,图 ...
- (转) Android开发性能优化简介
作者:贺小令 随着技术的发展,智能手机硬件配置越来越高,可是它和现在的PC相比,其运算能力,续航能力,存储空间等都还是受到很大的限制,同时用户对手机的体验要求远远高于PC的桌面应用程序.以上理由,足以 ...
- Android应用性能优化(转)
人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这 ...
- android 性能优化
本章介绍android高级开发中,对于性能方面的处理.主要包括电量,视图,内存三个性能方面的知识点. 1.视图性能 (1)Overdraw简介 Overdraw就是过度绘制,是指在一帧的时间内(16. ...
- 转载:SqlServer数据库性能优化详解
本文转载自:http://blog.csdn.net/andylaudotnet/article/details/1763573 性能调节的目的是通过将网络流通.磁盘 I/O 和 CPU 时间减到最小 ...
- 【转载】 Spark性能优化指南——基础篇
转自:http://tech.meituan.com/spark-tuning-basic.html?from=timeline 前言 开发调优 调优概述 原则一:避免创建重复的RDD 原则二:尽可能 ...
- JVM内存模型和性能优化 转
JVM内存模型和性能优化 JVM内存模型优点 内置基于内存的并发模型: 多线程机制 同步锁Synchronization 大量线程安全型库包支持 基于内存的并发机制,粒度灵活控制,灵活度高于 ...
随机推荐
- 面向对象的照妖镜——UML类图绘制指南
1.前言 感受 在刚接触软件开发工作的时候,每次接到新需求,在分析需求后的第一件事情,就是火急火燎的打开数据库(DBMS),开始进行数据表的创建工作.然而这种方式,总是会让我在编码过程中出现实体类设计 ...
- virtualbox的Linux虚拟磁盘大小调整及注意事项
virtualBox 调整磁盘分区 起因 起初安装centos6.3 时,没有修改默认的硬盘空间.只有8G,导致后面安装完zookeeper,jdk之后,在安装mysql发现磁盘空间不足 扩容步骤 1 ...
- 《吐血整理》高级系列教程-吃透Fiddler抓包教程(28)-Fiddler如何抓取Android7.0以上的Https包-下篇
1.简介 虽然依旧能抓到大部分Android APP的HTTP/HTTPS包,但是别高兴的太早,有的APP为了防抓包,还做了很多操作:① 二次加密有的APP,在涉及到关键数据通信时,会将正文二次加密后 ...
- wpf 手指触摸图片放大缩小 设置放大缩小值
xaml代码: <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/w ...
- javascript异步编程之generator(生成器函数)与asnyc/await语法糖
Generator 异步方案 相比于传统回调函数的方式处理异步调用,Promise最大的优势就是可以链式调用解决回调嵌套的问题.但是这样写依然会有大量的回调函数,虽然他们之间没有嵌套,但是还是没有达到 ...
- Java线程未捕获异常处理 UncaughtExceptionHandler
当一个线程在执行过程中抛出了异常,并且没有进行try..catch,那么这个线程就会终止运行.在Thread类中,提供了两个可以设置线程未捕获异常的全局处理器,我们可以在处理器里做一些工作,例如将异常 ...
- Day2.1
HelloWorld 随便新建一个文件夹,存放代码 新建一个java文件 文件后缀名为.java Hello.java 注意:系统可能没有显示文件名后缀,我们需要手动打开 编写代码 public cl ...
- 如何在bat中进入虚拟环境
很多情况下我们希望在项目中建立一个build.bat用于项目的自动构建,避免每次构建时都需要手动在控制台中输入命令. 例如对于 pyinstall 的项目,只需要如下的实现: pyinstaller ...
- JIRA操作之JQL
搜索功能 Jira的搜索功能非常强大,有专用的搜索语言JQL(Jira Query Language).Jira的Python库是基于JQL语法搜索的,返回的是搜索到的问题列表. jira.searc ...
- Spring Boot 中使用 tkMapper
说明:基于 MyBatis 有很多第三方功能插件,这些插件可以完成数据操作方法的封装.数据库逆向工程的生成等. tkMapper 和 MyBatis-plus 都是基于 MyBatis 提供的第三方插 ...