.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 大量线程安全型库包支持 基于内存的并发机制,粒度灵活控制,灵活度高于 ...
随机推荐
- 220514 T1 查询 (二分查找+vector)
用vector记录每个数出现的位置,对于要查询的X,要找他落在L~R的个数有几个,用lower_bound和upper_bound查找,相减就是答案. 1 #include<bits/stdc+ ...
- Docker | Compose创建mysql容器
本文通过Docker Compose来创建mysql容器 在linux服务器上创建文件,用于管理容器 mkdir docker-mysql cd docker-mysql vim docker-com ...
- 【python】Ubuntu中多条命令的运行
在模型训练时,往往需要消融实验,通常都是在一个程序运行结束后才再次手动运行下一个,不及时在电脑前可能无端浪费时间,因此需要让程序自动一个接一个去执行.受此启发,特了解了Ubuntu中多命令运行的内容. ...
- 【原创】i.MXRT J-Flash烧写算法使能eFuse熔丝位写入
临近年底,终于又憋了一篇文章出来,本来年初的时候是有计划把去年总结的一些东西整理下发布出来的,结果还是被工作和生活上各种琐事给耽搁了.哎,今年刚过了自己35岁的生日,眼瞅着这个人生节点 ...
- 2流高手速成记(之五):Springboot整合Shiro实现安全管理
废话不多说,咱们直接接上回 上一篇我们讲了如何使用Springboot框架整合Nosql,并于文章最后部分引入了服务端Session的概念 而早在上上一篇中,我们则已经讲到了如何使用Springboo ...
- FastApi学习
vscode配置 插件 code runner在 setting.json中关于python的修改为,因为我使用了虚拟环境,得让vscode找到python的路径 "code-runner. ...
- AtCoder Beginner Contest 277 题解
掉大分力(悲 A - ^{-1} 直接模拟. #include<bits/stdc++.h> #define IOS ios::sync_with_stdio(false) #define ...
- 4.drf-版本管理
根据RESTful规范,后端API中需要体现出版本,DRF中支持5种版本的设置,常见的三种如下 1. URL的GET中传递参数 在视图类中定义 from rest_framework.versioni ...
- C#使用不安全指针来操作bitmap
C#允许通过不安全指针实现像C++一样操作指针,这个速度是最快的.下面这个例子是修改一幅RGB图像的每个像素值,速度很快,测试一张2592*1944的彩色图像,只需要几ms就能够全部遍历. /// & ...
- 【云原生 · Docker】Docker虚拟化技术
1.Docker入门简介 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化. 容器是完全使用沙箱 ...