.NET使用Collections.Pooled提升性能优化的方法

目录
  • 简介
  • Collections.Pooled
    • 如何使用
    • 性能对比
      • PooledList<T>
      • PooledDictionary<TKey, TValue>
      • PooledSet<T>
      • PooledStack<T>
      • PooledQueue<T>
      • 未手动释放场景
    • 原理解析
  • 总结

简介

性能优化就是如何在保证处理相同数量的请求情况下占用更少的资源,而这个资源一般就是CPU或者内存,当然还有操作系统IO句柄、网络流量、磁盘占用等等。但是绝大多数时候,我们就是在降低CPU和内存的占用率。
之前分享的内容都有一些局限性,很难直接改造,今天要和大家分享一个简单的方法,只需要替换几个集合类型,就可以达到提升性能和降低内存占用的效果。
今天要给大家分享一个类库,这个类库叫Collections.Pooled,从名字就可以看出来,它是通过池化内存来达到降低内存占用和GC的目的,后面我们会直接来看看它的性能到底怎么样,另外也会带大家看看源码,为什么它会带来这些性能提升。

Collections.Pooled

项目链接:https://github.com/jtmueller/Collections.Pooled
该库基于System.Collections.Generic中的类,这些类已经被修改,以利用新的System.Span<T>System.Buffers.ArrayPool<T>类库,达到减少内存分配,提高性能,并允许与现代API的更大的互操作性的目的。
Collections.Pooled支持.NETStandard2.0(.NET Framework 4.6.1+),以及针对.NET Core 2.1+的优化构建。一套广泛的单元测试和基准已经从corefx移植过来。

测试总数:27501。通过:27501。失败:0。跳过:0。
测试运行成功。
测试执行时间:9.9019秒

如何使用

通过Nuget就可以很简单的安装这个类库,NuGet Version

Install-Package Collections.Pooled
dotnet add package Collections.Pooled
paket add Collections.Pooled

Collections.Pooled类库中,它针对我们常使用的集合类型都实现了池化的版本,和.NET原生类型的对比如下所示。

.NET原生 Collections.Pooled 备注
List<T> PooledList<T> 泛型集合类
Dictionary<TKey, TValue> PooledDictionary<TKey, TValue> 泛型字典类
HashSet<T> PooledSet<T> 泛型哈希集合类
Stack<T> Stack<T> 泛型栈
Queue<T> PooledQueue<T> 泛型队列

在使用时,我们只需要将对应的.NET原生版本换成Collections.Pooled版本就可以了,如下方的代码所示:

using Collections.Pooled;
// 使用方式是一样的
var list = new List<int>();
var pooledList = new PooledList<int>();
var dictionary = new Dictionary<int,int>();
var pooledDictionary = new PooledDictionary<int,int>();
// 包括PooledSet、PooledQueue、PooledStack的使用方法都是一样的
var pooledList1 = Enumerable.Range(0,100).ToPooledList();
var pooledDictionary1 = Enumerable.Range(0,100).ToPooledDictionary(i => i, i => i);

但是我们需要注意,Pooled类型实现了IDispose接口,它通过Dispose()方法将使用的内存归还到池中,所以我们需要在使用完Pooled集合对象以后调用它的Dispose()方法。或者可以直接使用using var关键字。

using Collections.Pooled;
// 使用using var 会在pooled对象使用完毕后自动释放
using var pooledList = new PooledList<int>();
Console.WriteLine(pooledList.Count);
// 使用using作用域 作用域结束以后就会释放
using (var pooledDictionary = new PooledDictionary<int, int>())
{
	Console.WriteLine(pooledDictionary.Count);
}
// 手动调用Dispose方法
var pooledStack = new PooledStack<int>();
Console.WriteLine(pooledStack.Count);
pooledList.Dispose();

注意:使用Collections.Pooled内的集合对象最好需要释放掉它,不过不释放也没有关系,GC最终会回收它,只是它不能归还到池中,达不到节省内存的效果了。
由于它会复用内存空间,在将内存空间返回到池中的时候,需要对集合内的元素做处理,它提供了一个叫ClearMode的枚举供使用,定义如下:

namespace Collections.Pooled
{
    /// <summary>
    /// 这个枚举允许控制在内部数组返回到ArrayPool时如何处理数据。
    /// 数组返回到ArrayPool时如何处理数据。在使用默认选项之外的其他选项之前,请注意了解
    /// 在使用默认值Auto之外的任何其他选项之前,请仔细了解每个选项的作用。
    /// </summary>
    public enum ClearMode
    {
        /// <summary>
        /// <para><code>Auto</code>根据目标框架有不同的行为</para>
        /// <para>.NET Core 2.1: 引用类型和包含引用类型的值类型在内部数组返回池时被清除。 不包含引用类型的值类型在返回池时不会被清除。</para>
        /// <para>.NET Standard 2.0: 在返回池之前清除所有用户类型,以防它们包含引用类型。 对于 .NET Standard,Auto 和 Always 具有相同的行为。</para>
        /// </summary>
        Auto = 0,

        /// <summary>
        /// The <para><code>Always</code> 设置的效果是在返回池之前总是清除用户类型。
        /// </summary>
        Always = 1,

        /// <summary>
        /// <para><code>Never</code> 将导致池化集合在将它们返回池之前永远不会清除用户类型。</para>
        /// </summary>
        Never = 2
    }
}

默认情况下,使用默认值Auto即可,如果有特殊的性能要求,知晓风险后可以使用Never。
对于引用类型和包含引用类型的值类型,我们必须在将内存空间归还到池的时候清空数组引用,如果不清除会导致GC无法释放这部分内存空间(因为元素的引用一直被池持有),如果是纯值类型,那么就可以不清空,在使用结构体替代类这篇文章中,我描述了引用类型和结构体(值类型)数组的存储区别,纯值类型没有对象头回收也无需GC介入。

性能对比

我没有单独做Benchmark,直接使用的开源项目的跑分结果,很多项目的内存占用都是0,那是因为使用的池化的内存,没有多余的分配。

PooledList<T>

在Benchmark中循环向集合添加2048个元素,.NET原生的List<T>需要110us(根据实际跑分结果,图中的毫秒应该是笔误)和263KB内存,而PooledList<T>只需要36us和0KB内存。

PooledDictionary<TKey, TValue>

在Benchmark中循环向字典添加10_0000个元素,.NET原生的Dictionary<TKey, TValue>需要11ms和13MB内存,而PooledDictionary<TKey, TValue>只需要7ms和0MB内存。

PooledSet<T>

在Benchmark中循环向哈希集合添加10_0000个元素,.NET原生的HashSet<T>需要5348ms和2MB,而PooledSet<T>只需要4723ms和0MB内存。

PooledStack<T>

在Benchmark中循环向栈添加10_0000个元素,.NET原生的PooledStack<T>需要1079ms和2MB,而PooledStack<T>只需要633ms和0MB内存。

PooledQueue<T>

在Benchmark中循环向队列添加10_0000个元素,.NET原生的PooledQueue<T>需要681ms和1MB,而PooledQueue<T>只需要408ms和0MB内存。

未手动释放场景

另外在上文中我们提到了Pooled的集合类型需要释放,但是不释放也没有太大的关系,因为GC会去回收。

private static readonly string[] List = Enumerable
    .Range(0, 10000).Select(c => c.ToString()).ToArray();
// 使用默认的集合类型
[Benchmark(Baseline = true)]
public int UseList()
{
    var list = new List<string>(1024);
    for (var index = 0; index < List.Length; index++)
    {
        var item = List[index];
        list.Add(item);
    }
    return list.Count;
}
// 使用PooledList 并且及时释放
[Benchmark]
public int UsePooled()
{
    using var list = new PooledList<string>(1024);
    for (var index = 0; index < List.Length; index++)
    {
        var item = List[index];
        list.Add(item);
    }
    return list.Count;
}
// 使用PooledList 不释放
[Benchmark]
public int UsePooledWithOutUsing()
{
    var list = new PooledList<string>(1024);
    for (var index = 0; index < List.Length; index++)
    {
        var item = List[index];
        list.Add(item);
    }
    return list.Count;
}

Benchmark结果如下:

可以从上面的Benchmark结果可以得出结论。

  • 及时释放Pooled类型集合几乎不会触发GC和分配内存,从上图中它只分配了56Byte内存。
  • 就算不释放Pooled类型集合,因为它从池中分配内存,在进行ReSize扩容操作时还是会复用内存,另外跳过了GC分配内存初始化步骤,速度也比较快。
  • 最慢的就是使用普通集合类型,每次ReSize扩容操作都需要申请新的内存空间,GC也要回收之前的内存空间。

原理解析

如果大家看过我之前的博文你应该为集合类型设置初始大小和浅析C# Dictionary实现原理就可以知道,.NET BCL开发人员为了高性能的随机访问,这些基本集合类型的底层数据结构都是数组,我们以List<T>为例。

  • 创建新的数组来存储添加进来的元素。
  • 如果数组空间不够,那么就触发扩容操作,申请2倍的空间大小。

构造函数代码如下,可以看到是直接创建的泛型数组:

public List(int capacity)
{
      if (capacity < 0)
          ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);

      if (capacity == 0)
          _items = s_emptyArray;
      else
          _items = new T[capacity];
}

那么如果想要池化内存,只需要把类库中使用new关键字申请的地方,改为使用池化的申请。这里和大家分享.NET BCL中的一个类型,叫ArrayPool,它提供了可重复使用的泛型实例的数组资源池,使用它可以降低对GC的压力,在频繁创建和销毁数组的情况下提升性能。
而我们Pooled类型的底层就是使用ArrayPool来共享资源池,从它的构造函数中,我们可以看到它默认使用的是ArrayPool<T>.Shared来分配数组对象,当然你也可以创建自己的ArrayPool来让它使用。

// 默认使用ArrayPool<T>.Shared池
public PooledList(int capacity, ClearMode clearMode, bool sizeToCapacity) : this(capacity, clearMode, ArrayPool<T>.Shared, sizeToCapacity) { }  

// 分配数组使用 ArrayPool
public PooledList(int capacity, ClearMode clearMode, ArrayPool<T> customPool, bool sizeToCapacity)
{
    if (capacity < 0)
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
    _pool = customPool ?? ArrayPool<T>.Shared;
    _clearOnFree = ShouldClear(clearMode);
    if (capacity == 0)
    {
        _items = s_emptyArray;
    }
    else
    {
        _items = _pool.Rent(capacity);
    }

    if (sizeToCapacity)
    {
        _size = capacity;
        if (clearMode != ClearMode.Never)
        {
            Array.Clear(_items, 0, _size);
        }
    }
 }

另外在进行容量调整操作(扩容)时,会将旧的数组归还回线程池,新的数组也在池中获取。

public int Capacity
{
    get => _items.Length;
    set
    {
        if (value < _size)
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
        }

        if (value != _items.Length)
        {
            if (value > 0)
            {
                // 从池中分配数组
                var newItems = _pool.Rent(value);
                if (_size > 0)
                {
                    Array.Copy(_items, newItems, _size);
                }
                // 旧数组归还到池中
                ReturnArray();
                _items = newItems;
            }
            else
            {
                ReturnArray();
                _size = 0;
            }
        }
    }
}
private void ReturnArray()
{
    if (_items.Length == 0)
        return;
    try
    {
        // 归还到池中
        _pool.Return(_items, clearArray: _clearOnFree);
    }
    catch (ArgumentException)
    {
        // ArrayPool可能会抛出异常,我们直接吞掉
    }
    _items = s_emptyArray;
}

另外作者使用了Span优化了AddInsert等等API,让它们有更好的随机访问性能;另外还加入了TryXXX系列API,可以更方便的方式的使用它。比如List<T>类相比PooledList<T>就有多达170个修改。

总结

在我们线上实际的使用过程中,完全可以用Pooled提供的集合类型替代原生的集合类型,对降低内存占用率和P95延时有非常大的帮助。
另外就算忘记释放了,那性能也不会比使用原生的集合类型差多少。当然最好的习惯就是及时的释放它。

到此这篇关于.NET使用Collections.Pooled性能优化的方法的文章就介绍到这了,更多相关.net性能优化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • asp.net性能优化之使用Redis缓存(入门)

    1:使用Redis缓存的优化思路 redis的使用场景很多,仅说下本人所用的一个场景: 1.1对于大量的数据读取,为了缓解数据库的压力将一些不经常变化的而又读取频繁的数据存入redis缓存 大致思路如下:执行一个查询 1.2首先判断缓存中是否存在,如存在直接从Redis缓存中获取. 1.3如果Redis缓存中不存在,实时读取数据库数据,同时写入缓存(并设定缓存失效的时间). 1.4缺点,如果直接修改了数据库的数据而又没有更新缓存,在缓存失效的时间内将导致读取的Redis缓存是错误的数据. 2:R

  • .NET使用结构体替代类提升性能优化的技巧

    目录 前言 现实的案例 内存占用 计算速度 总结 附录 前言 我们知道在C#和Java明显的一个区别就是C#可以自定义值类型,也就是今天的主角struct,我们有了更加方便的class为什么微软还加入了struct呢?这其实就是今天要谈到的一个优化性能的Tips使用结构体替代类.那么使用结构体替代类有什么好处呢?在什么样的场景需要使用结构体来替代类呢?今天的文章为大家一一解答.注意:本文全部都以x64位平台为例 现实的案例 举一个现实系统的例子,大家都知道机票购票的流程,开始选择起抵城市和机场(

  • .NET性能优化之为结构体数组使用StructLinq的问题解析

    目录 前言 Linq是值传递 使用StructLinq 引入StructLinq 简单使用 性能 在上文场景中使用 总结 前言 本系列的主要目的是告诉大家在遇到性能问题时,有哪些方案可以去优化:并不是要求大家一开始就使用这些方案来提升性能.在之前几篇文章中,有很多网友就有一些非此即彼的观念,在实际中,处处都是开发效率和性能之间取舍的艺术.<计算机编程艺术>一书中提到过早优化是万恶之源,在进行性能优化时,你必须要问自己几个问题,看需不要进行性能优化. 优化的成本高么? 如果立刻开始优化会带来什么

  • .NET性能优化之为集合类型设置初始大小的方法

    目录 前言 集合类型 List源码 Queue.Stack源码 HashSet.Dictionary源码 总结 附录 前言 计划开一个新的系列,来讲一讲在工作中经常用到的性能优化手段.思路和如何发现性能瓶颈,后续有时间的话应该会整理一系列的博文出来.今天要谈的一个性能优化的Tips是一个老生常谈的点,但是也是很多人没有注意的一个点.在使用集合类型是,你应该设置一个预估的初始大小,那么为什么需要这样做?我们一起来从源码的角度说一说. 集合类型 我们先来聊一聊.NET BCL库中提供的集合类型,对于

  • ASP.NET比较常用的26个性能优化技巧

    本篇文章主要介绍了"ASP.NET中常用的26个优化性能方法",主要涉及到ASP.NET中常用的26个优化性能方法方面的内容,对于ASP.NET中常用的26个优化性能方法感兴趣的同学可以参考一下. 现在很多客户也慢慢开始注重网站的性能了,同时有很多运营网站的公司也不像以前那样特别在意网站是否非常漂亮,而把更多的精力放在了网站性能优化上面,提供更快更稳定的浏览速度,在这个基础上面进行网站功能上的扩充和完善,那么在asp.net中如何优化性能呢? 1. 数据库访问性能优化 数据库的连接和关

  • .NET使用Collections.Pooled提升性能优化的方法

    目录 简介 Collections.Pooled 如何使用 性能对比 PooledList<T> PooledDictionary<TKey, TValue> PooledSet<T> PooledStack<T> PooledQueue<T> 未手动释放场景 原理解析 总结 简介 性能优化就是如何在保证处理相同数量的请求情况下占用更少的资源,而这个资源一般就是CPU或者内存,当然还有操作系统IO句柄.网络流量.磁盘占用等等.但是绝大多数时候,我

  • Vue性能优化的方法

    今天来谈一谈Vue中一些性能优化的问题,仅仅是个人使用中的一些小心得,来,今天我一句废话不多说,直接上内容好吧 1.v-if和v-show的使用, 我们都知道这两个都可以控制显隐,那我们用哪个呢,个人觉得要从两个方面入手来确定使用哪个, 1.权限的问题,只要涉及到权限相关的展示用v-if比较好 2.切换地频率,如果频繁的切换我们用v-show,不频繁的切换用v-if 其实两者各有优缺,就看你是怎么选择了,用v-if能减少页面中的DOM总数,加快渲染的速度,而且我们要清楚一个事情 v-if是'真正

  • 浅谈react性能优化的方法

    React性能优化思路 软件的性能优化思路就像生活中去看病,大致是这样的: 使用工具来分析性能瓶颈(找病根) 尝试使用优化技巧解决这些问题(服药) 使用工具测试性能是否确实有提升(疗效确认) 初识react只是为了尽快完成项目,后期进行代码审查时候发现有很多地方需要优化,因此做了个小结. Code Splitting shouldComponentUpdate避免重复渲染 使用不可突变数据结构 组件尽可能的进行拆分.解耦 列表类组件优化 bind函数优化 不要滥用props ReactDOMSe

  • angularjs性能优化的方法

    学习angularjs有一段时间了,但是一直都没有怎么考虑过性能方面的问题,上次在研究过滤器的时候涉及到了性能问题.所以自己也总结了下常用的性能优化. 优化$watch 1.及时移除不必要的watch var unWatch = $scope.$watch('', function() { // do something ... if (someCondition) { unWatch(); // 取消监听 } }); 2.尽量避免深度watch 我们都知道$watch有三个参数,第三个参数为t

  • JS性能优化实现方法及优点进行

    最近刚阅读完<高性能javascript>,想谈谈对js性能优化的看法.理解有些不同,可能还需要各位多多提醒. 话不多说,提到javascript难免会联想到文档对象模型(DOM),它作用于XML和HTML文档的程序接口(API),位于浏览器中,主要用来与HTML文档打交道.同样也用于Web程序中获取XML文档,并使用DOM API来访问文档中的数据.尽管DOM是个与语言无关的API,它在浏览器中的接口却是用javascript实现的.客户端脚本编程大多数时候是在和底层文档(underlyin

  • 小程序多图列表实现性能优化的方法步骤

    写这篇文章的缘由: 最近在公司的小程序项目中遇到了页面图片元素过多导致的性能问题. 从小程序提供的性能检测面板分析, 确定是图片元素占用了过多内存导致. 因为本人之前主要是做桌面端应用开发和原生app开发, 没有太顾及过移动端图片的内存占用问题. 这次既然遇到了, 也就趁这个机会学习一下其优化的技巧. 什么造成的性能问题 简单的来说: DOM节点过多 && 图片节点过多 DOM节点过多会造成更多的内存占用. 按照目前的微信小程序限制, 内存占用500M以上会出现卡顿, 甚至闪退. 如果列表

  • 50个PHP程序性能优化的方法

    1. 用单引号代替双引号来包含字符串,这样做会更快一些.因为 PHP 会在双引号包围的 字符串中搜寻变量,单引号则不会,注意:只有 echo 能这么做,它是一种可以把多个字符 串当作参数的"函数"(译注:PHP 手册中说 echo 是语言结构,不是真正的函数,故把函数 加上了双引号). 2.如果能将类的方法定义成 static,就尽量定义成 static,它的速度会提升将近 4 倍. 3.$row['id'] 的速度是$row[id]的 7 倍. 4.echo 比 print 快,并且

  • 前端从浏览器的渲染到性能优化

    问题前瞻 1. 为什么css需要放在头部? 2. js为什么要放在body后面? 3. 图片的加载和渲染会阻塞页面DOM构建吗? 4. dom解析完才出现页面吗? 5. 首屏时间根据什么来判定? 浏览器渲染 1.浏览器渲染图解 浏览器渲染页面主要经历了下面的步骤: 1.处理 HTML 标记并构建 DOM 树. 2.处理 CSS 标记并构建 CSSOM 树. 3.将 DOM 与 CSSOM 合并成一个渲染树. 4.根据渲染树来布局,以计算每个节点的几何信息. 5.将各个节点绘制到屏幕上. 为构建渲

  • Android性能优化方法

    GPU过度绘制 •打开开发者选型,"调试GPU过度绘制",蓝.绿.粉红.红,过度绘制依次加深  •粉红色尽量优化,界面尽量保持蓝绿颜色  •红色肯定是有问题的,不能忍受 使用HierarchyView分析布局层级 •删除多个全屏背景:应用中不可见的背景,将其删除掉  •优化ImageView:对于先绘制了一个背景,然后在其上绘制了图片的,9-patch格式的背景图中间拉伸部分设置为透明的,Android 2D渲染引擎会优化9-patch图中的透明像素.这个简单的修改可以消除头像上的过度

随机推荐