提到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的更多相关文章

  1. 【腾讯Bugly干货分享】Android性能优化典范——第6季

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/580d91208d80e49771f0a07c 导语 这里是Android性能优 ...

  2. Unity性能优化(3)-官方教程Optimizing garbage collection in Unity games翻译

    本文是Unity官方教程,性能优化系列的第三篇<Optimizing garbage collection in Unity games>的翻译. 相关文章: Unity性能优化(1)-官 ...

  3. 分享10条PHP性能优化的小技巧,帮助你更好的用PHP开发:

    1. foreach效率更高,尽量用foreach代替while和for循环. 2. 循环内部不要声明变量,尤其是对象这样的变量. 3. 在多重嵌套循环中,如有可能,应当将最长的循环放在内层,最短循环 ...

  4. 阿里无线前端性能优化指南 (Pt.1 加载优化)

    前言 阿里无线前端团队在过去一年对所负责业务进行了全面的性能优化.以下是我们根据实际经验总结的优化指南,希望对大家有所帮助. 第一部分仅包括数据加载期优化. 图片控制 对于网页特别是电商类页面来说,图 ...

  5. (转) Android开发性能优化简介

    作者:贺小令 随着技术的发展,智能手机硬件配置越来越高,可是它和现在的PC相比,其运算能力,续航能力,存储空间等都还是受到很大的限制,同时用户对手机的体验要求远远高于PC的桌面应用程序.以上理由,足以 ...

  6. Android应用性能优化(转)

    人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这 ...

  7. android 性能优化

    本章介绍android高级开发中,对于性能方面的处理.主要包括电量,视图,内存三个性能方面的知识点. 1.视图性能 (1)Overdraw简介 Overdraw就是过度绘制,是指在一帧的时间内(16. ...

  8. 转载:SqlServer数据库性能优化详解

    本文转载自:http://blog.csdn.net/andylaudotnet/article/details/1763573 性能调节的目的是通过将网络流通.磁盘 I/O 和 CPU 时间减到最小 ...

  9. 【转载】 Spark性能优化指南——基础篇

    转自:http://tech.meituan.com/spark-tuning-basic.html?from=timeline 前言 开发调优 调优概述 原则一:避免创建重复的RDD 原则二:尽可能 ...

  10. JVM内存模型和性能优化 转

    JVM内存模型和性能优化 JVM内存模型优点 内置基于内存的并发模型:      多线程机制 同步锁Synchronization 大量线程安全型库包支持 基于内存的并发机制,粒度灵活控制,灵活度高于 ...

随机推荐

  1. JVM中的堆

    堆 内存结构 堆的核心概念 <java虚拟机规范>中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上. 一个JVM实例只存在一个堆内存(就是new 出来一个对象),ja ...

  2. Conda的使用

    conda常用的命令 在Anaconda Powershell Prompt 输入: 1.conda -V检验是否安装及当前conda的版本. 2.conda list查看安装了哪些包 3.conda ...

  3. Python 根据两个字段排序 中文排序 汉字排序 升序 降序

    Python3写法 代码 # -*- coding: utf-8 -*- # 需求:年龄倒序,姓名正序 from itertools import chain from pypinyin import ...

  4. 6.channels 配置websocket

      Django默认不支持websockey,需要Django支持的话需要安装第三方组件 django channels 是django支持websocket的一个模块.   1.安装 pip3 in ...

  5. JS逆向实战1——某省阳光采购服务平台

    分析 其实这个网站基本没有用到过什么逆向,就是简单的图片base64加密 然后把连接变成2进制存成文件 然后用ocr去识别即可 !! 注意 在获取图片连接 和对列表页发起请求时一定要用一个请求,也就是 ...

  6. Codeforces Round #832 (Div. 2) A-D

    比赛链接 A 题解 知识点:贪心. 我们考虑把正数和负数分开放,显然把负数和正数放在一起的结果不会更优. 时间复杂度 \(O(n)\) 空间复杂度 \(O(1)\) 代码 #include <b ...

  7. Jekyll于windows中使用

    安装 安装Ruby http://rubyinstaller.org/downloads/ 于其中选择最新的带dev套件的. 在安装时,安装目录不能有空格,检查是否已经安装成功 ruby -v gem ...

  8. AGC007C Pushing Balls —— 期望的神题

    Problem Link 题意: 序列上按顺序交错有 \(n\) 个球和 \(n+1\) 个洞,即 \(hole_1,ball_1,hole_2,ball_2,\dots,ball_n,hole_{n ...

  9. jupyter notebook使用相对路径的方法

    在当前文件夹路径下开启jupyter notebook 这样就可以直接使用相对路径了,而不用管绝对路径这一令人心烦的问题 首先需要重新安装PowerShell 下载链接:https://cloud.1 ...

  10. python中的super()是什么?

    技术场景:python中的super,名为超类,可以简单的理解为执行父类的__init__函数.由于在python中不论是一对一的继承,还是一子类继承多个父类,都会涉及到执行的先后顺序的问题.那么本文 ...