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

ArrayPool,性能,对象,NET · 浏览次数 : 2198

小编点评

## ArrayPool 的复用优化方案 **ArrayPool 的原理** ArrayPool 是一种自动内存池,它可以帮助您将数组中的对象缓存起来,从而减少内存使用并提高性能。然而,在使用 ArrayPool 时,您可能需要考虑如何复用数组中的对象。 **复用数组的方式** * **`ArrayPool.Rent()`** 函数可以用来获取一个新的 ArrayPool 对象,并将其用于存储您之前的数组对象。 * **`ArrayPool.Return()`** 函数可以用来将现有 ArrayPool 对象的所有对象释放回内存池。 **复用对象的性能** 在复用对象时,您可以使用以下方法来提高性能: * **只复用对象当它们是空时**。如果您可以确定数组中的所有对象都是空的,您可以使用 `ArrayPool.Rent(objectCount)` 来创建新的 ArrayPool 对象。 * **使用缓存机制**。您可以使用缓存机制来缓存最近使用过的对象。这可以帮助您减少对 ArrayPool 的初始化和清理操作的次数。 * **使用性能分析工具**。您可以使用性能分析工具,如 APM 和 dotnet tools,来跟踪您的应用程序的性能,并找到性能瓶颈。 **示例** ```csharp // 创建一个新的 ArrayPool 对象 var pool = ArrayPool.Create(100); // 获取第一个对象 var firstObject = pool.Rent(); // 释放对象 pool.Return(firstObject); // 使用缓存机制 var secondObject = pool.Rent(); pool.Return(secondObject); // 使用性能分析工具 // ... ``` **结论** ArrayPool 是一个非常有效的性能优化技术,但是在复用对象时,您需要考虑如何优化性能。通过使用性能分析工具,您可以找到性能瓶颈并开发方法来提高性能。

正文

前两天在微信后台收到了读者的私信,问了一个这样的问题,由于私信回复有字数和篇幅限制,我在这里统一回复一下。读者的问题是这样的:

大佬您好,之前读了您的文章受益匪浅,我们有一个项目经常占用 7-8GB 的内存,使用了您推荐的ArrayPool以后降低到 4GB 左右,我还想着能不能继续优化,于是 dump 看了一下,发现是ArrayPool对应的一个数组有几万个对象,这个类有 100 多个属性。我想问有没有方法能复用这些对象?感谢!

根据读者的问题,我们摘抄出重点,现在他的数组已经得到池化,但是数组里面存的对象很大,从而导致内存很大

我觉得一个类有 100 多个属性应该是不太正常的,当然也可能是报表导出之类的需求,如果是普通类有 100 多个属性,那应该做一些抽象和拆分了。

如果是少部分的大对象需要重用,那其实可以使用ObjectPool,如果是数万个对象要重用,那么ObjectPool里面的 CAS 算法会成为瓶颈,那有没有更好的方式呢?其实解决方案就在ArrayPool类本身,可能大家平时没有注意过。

再聊 ArrayPool

我们再来回顾一下ArrayPool的用法,它的用法很简单,核心就是RentReturn两个方法,演示代码如下所示:

using System.Buffers;

namespace BenchmarkPooledList;

public class ArrayPoolDemo
{
    public void Demo()
    {
        // get array from pool
        var pool = ArrayPool<byte>.Shared.Rent(10);
        try
        {
            // do something
        }
        finally
        {
            // return
            ArrayPool<byte>.Shared.Return(pool);
        }
    }
}

其实对于上面的这个问题,ArrayPool已经有了解决方案,不知道大家有没有注意Return方法有一个默认参数clearArray=false.

public abstract void Return (T[] array, bool clearArray = false);

其中clearArray的含义就是当数组被归还到池时,是不是清空数组,也就是会不会将数组的所有元素重置为null,看下面的例子就明白了。

可以发现只要在归还到数组时不清空,那么第二次拿到的数组还是会保留值,基于这样一个设计,我们就可以在复用数组的同时复用对应的元素对象

性能比较

那么这样是否能解决之前提到的问题呢?我们很简单就可以构建一个测试用例,一个在代码里面使用new每次创建对象,另外一个尽量复用对象,为null时才创建。

// 定义一个大对象,放了40个属性
public class BigObject
{
    public string P1 { get; set; }
    public string P2 { get; set; }
    public string P3 { get; set; }
    .....
}

然后创建一个数据集,生成1000条数据,使用默认的方式,每次都new对象。

private static readonly string[] Datas = Enumerable.Range(0, 1000).Select(c => c.ToString()).ToArray();

[Benchmark(Baseline = true)]
public long UseArrayPool()
{
    var pool = ArrayPool<BigObject?>.Shared.Rent(Datas.Length);
    try
    {
        for (int i = 0; i < Datas.Length; i++)
        {
            pool[i] = new BigObject
            {
                P1 = Datas[i],
                P2 = Datas[i],
                P3 = Datas[i]
                // .... 省略赋值代码
            };
        }

        return pool.Length;
    }
    finally
    {
        ArrayPool<BigObject?>.Shared.Return(pool);
    }
}

另外一种方式就是复用对象池的对象,只有为null时才创建:

[Benchmark]
public long UseArrayPoolNeverClear()
{
    var pool = ArrayPool<BigObject?>.Shared.Rent(Datas.Length);
    try
    {
        for (int i = 0; i < Datas.Length; i++)
        {
            // 复用obj 为null时才创建
            var obj = pool[i] ?? (pool[i] = new BigObject());
            obj.P1 = Datas[i];
            obj.P2 = Datas[i];
            obj.P3 = Datas[i];
            // .... 省略赋值代码
        }

        return pool.Length;
    }
    finally
    {
        ArrayPool<BigObject?>.Shared.Return(pool, false);
    }
}

可以看一下 Benchmark 的结果:

复用大对象的场景下,在没有造成性能的下降的情况下,内存分配几乎为0

ArrayObjectPool

之前笔者实现了一个类,优化了一下上面代码的性能,但是之前换了电脑,没有备份一些杂乱数据,现在找不到了。

具体优化原理是每一次都要进行null比较还是比较麻烦,而且如果能确定其数组不变的话,这些 null 判断是可以移除的。

凭借记忆写了一个 Demo,主要是确立在池里的数组是私有的,初始化一次以后就不需要再初始化,所以只要检测第一个元素是否为null就行,实现如下所示:

// 应该要实现IList<T>接口 和 ICollection<T> 等等的接口
// 不过这只是简单的demo  各位可以自行实现
public class ArrayObjectPool<T> : IDisposable // , IList<T>
    where T : new()
{
    // 创建一个独享的池
    private static ArrayPool<T> _pool = ArrayPool<T>.Create();

    private readonly T[] _items;
    public ArrayObjectPool(int size)
    {
        Length = size;
        _items = _pool.Rent(size);
        if (_items[0] is not null) return;
        // 如果第一个元素为null 说明是没初始化的
        // 那么需要初始化
        for (int i = 0; i < _items.Length; i++)
        {
            _items[i] = new T();
        }
    }

    // 为了安全只实现get
    public T this[int index]
    {
        get
        {
            if (index < 0 || index > Length)
                throw new ArgumentOutOfRangeException(nameof(index));
            return _items[index];
        }
        set => throw new NotSupportedException();
    }


    public int Length { get; }

    // 释放时返回数据
    public void Dispose()
    {
        _pool.Return(_items);
    }

    /// <summary>
    /// 当ArrayPool过大时  可以重新创建
    /// 旧的池就会被GC 回收
    /// </summary>
    public static void Flush()
    {
        _pool = ArrayPool<T>.Create();
    }
}

同样的,对比了一下性能,因为会创建一个对象,所以内存占用比直接使用ArrayPool要高几十个字节,但是由于不用比较null,是实现里面最快的(当然也快不了多少,就 2%):

总结

我相信这个应该已经能回答提出的问题,我们可以在复用数组的时候复用数组所对应的对象,当然你必须确保复用对象没有副作用,比如复用了旧的脏数据

如果不是经常写这样的代码,像笔者一样封装一个ArrayObjectPool也没有必要,笔者本人也就写过那么一次,如果经常有这样的场景,那可以封装一个安全的ArrayObjectPool,想必也不是什么困难的事情。

感谢阅读,如果您有什么关于性能优化的疑问,欢迎在公众号留言。

.NET 性能优化交流群

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

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

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

与.NET性能优化-ArrayPool同时复用数组和对象相似的内容:

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

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

.NET性能优化-复用StringBuilder

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

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

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

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

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

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

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

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

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

.NET遍历二维数组-先行/先列哪个更快?

上周在.NET性能优化群里面有一个很有意思的讨论,讨论的问题如下所示: 请教大佬:2D数组,用C#先遍历行再遍历列,或者先遍历列再遍历行,两种方式在性能上有区别吗? 据我所知,Julia或者python的 pandas,一般建议先遍历列,再遍历行 在群里面引发了很多大佬的讨论,总的来说观点分为以下三

【.NET8】访问私有成员新姿势UnsafeAccessor(上)

前言 前几天在.NET性能优化群里面,有群友聊到了.NET8新增的一个特性,这个类叫UnsafeAccessor,有很多群友都不知道这个特性是干嘛的,所以我就想写一篇文章来带大家了解一下这个特性。 其实在很早之前我就有关注到这个特殊的特性,但是当时.NET8还没有正式发布,所以我也没有写文章,现在.

【转帖】Linux性能优化(一)——stress压力测试工具

https://blog.csdn.net/a642960662/category_11641226.html 一、stress简介 1、stress简介 stress是Linux的一个压力测试工具,可以对CPU、Memory、IO、磁盘进行压力测试。 2、stress安装 安装: sudo yum

[转帖]【建议收藏】15755 字,讲透 MySQL 性能优化(包含 MySQL 架构、存储引擎、调优工具、SQL、索引、建议等等)

https://my.oschina.net/jiagoushi/blog/5593246 0. 目录 1)MySQL 总体架构介绍 2)MySQL 存储引擎调优 3)常用慢查询分析工具 4)如何定位不合理的 SQL 5)SQL 优化的一些建议 1 MySQL 总体架构介绍 1.1 MySQL 总体