.NET性能优化-推荐使用Collections.Pooled(补充)
简介
在上一篇.NET性能优化-推荐使用Collections.Pooled一文中,提到了使用Pooled
类型的各种好处,但是在群里也有小伙伴讨论了很多,提出了很多使用上的疑问。
所以特此写了这篇文章,补充回答小伙伴们问到的一些问题,和遇到某些场景如何处理。
问题分析
以下就是这两天收集到比较常见的问题,我都收集到一起,统一给大家回复一下。
ArrayPool会不会无限扩大?
遇到的第一个问题就是我们Pooled
类型依赖于ArrayPool
进行底层数组的分配,那么我们一直使用Pooled
类型会不会导致ArrayPool
无限制的扩大下去?
回答:不会无限制的扩大,ArrayPool
在.NET BCL库中有两种实现:
- 一种是调用
ArrayPool<T>.Shared
的使用Thread-local storage
方式实现的池,名称叫做TlsOverPerCoreLockedStacksArrayPool
,这种池的话通过核心隔离使得在并发情况下的性能非常好,如果后面又时间我会出一篇源码解析的文章,它里面会限制池中对象最大的数量。 - 第二种是调用
ArrayPool<T>.Create()
方法创建的池,这种会单独使用另一个类,叫ConfigurableArrayPool
,可以在池对象创建的时候就指定构造函数的参数,达到限制大小的目的。
Dispose会不会影响性能?
另外有小伙伴比较关注的问题就是,对于Pooled
里面提供的类型都实现了IDisposable
接口,那么频繁的Dispose会不会影响性能呢?
回答: 先说结论,结论就是对性能的负面影响微乎其微。我们从两个方面来回答一下这个问题:
- 其实实现
IDisposable
接口没有什么特殊的,就是要求类中需要有一个Dispose
方法而已,和你自定义一个IFoo
接口,整一个Foo
类来实现这个接口一个意思。但是你需要注意实现了析构方法的场景,这个场景GC会将实例加入终结器队列,但是Pooled
提供的类库中都没有实现析构方法,不存在这个问题。 - 第二点就是如果要归还池化的对象,那么你需要手动调用
Dispose
方法或者使用using
在作用域中自动调用Dispose
方法,大家都知道调用一个方法会有性能开销,这个是肯定的。但是比起重新申请内存时GC需要初始化内存和回收内存来说,这些开销微乎其微。
可以将所有对象都池化吗?
既然池化的效果这么好,那么我可以将所有的对象都池化吗?这样是不是就没有了GC开销,更能节省性能呢?
回答: 可以将所有对象都池化,必要性不大。
因为我们对于池化能够提升性能是建立在对象创建的开销比重用大的这一个前提下,但是这就需要分场景讨论了,比如我们创建一个很大的数组,对于数组来说GC需要申请内存空间,初始化内存和回收内存,开销远远比重用大,所以此时池化对象是有很大的正面收益的;但是反观另外一个场景,就是一个只包含了几个字段的类,GC能很快的创建和回收它,这样可能收益会变小,甚至成为负优化。
最后还有一个问题就是,申请和归还到池中的时候,是需要线程安全的操作,因为同时间可能有很多线程在申请和归还对象,这时就会引入一些线程同步的问题,就算使用一些无锁算法,在高并发的情况下实际测试表现也不如GC来的快。
我这个场景如何使用Pooled?
1.创建的集合对象作用域不在一个方法内,应该怎么办? 样例代码如下所示:
// BLL层逻辑
int Get()
{
var pooledList = Get1();
// 省略代码逻辑
return pooledList.Sum();
}
// DAL层逻辑
// 数据集合在其它方法创建
PooledList<int> Get1()
{
// 省略代码逻辑
// 创建的Pooled数组没办法在这个方法里面回收
return pooledList;
}
这种其实很简单,只要在BLL层的Get
方法中释放或者调用Dispose
方法就可以了。也就是说在它最后被使用到的地方释放,而不是声明它的地方。
// BLL层逻辑
int Get()
{
// 在这个方法里面用using var释放就可以了
using var pooledList = Get1();
// 省略代码逻辑
return pooledList.Sum();
}
2.底层接口返回的是一个IList<T>
怎么办? 代码如下所示:
// BLL层逻辑
int Get()
{
// 报错
// 底层返回的是 IList没有实现IDispose方法,
using var pooledList = Get1();
// 省略代码逻辑
return pooledList.Sum();
}
// DAL层逻辑
// 虽然实际类型是PooledList,但是接口契约是IList
IList<int> Get1()
{
// 省略代码逻辑
return pooledList;
}
这种的话确实改造起来麻烦一点,如果不想改变接口契约,那就普通方式的话在Get
方法中加判断逻辑了,另外也可以用后面提到的Dispose.Scope
类库。
// BLL层逻辑
int Get()
{
var pooledList = Get1();
// 加一个转换
using var _ = (pooledList as IDisposable)
// 省略代码逻辑
return pooledList.Sum();
}
3.在AspNetCore的ApiController的Action能用Pooled类吗?怎么回收Pooled类呢? 代码如下所示:
[ApiController]
public TestController : Controller
{
[HttPost]
PooledList<int> GetSome() => BLL.GetSome();
}
像这种情况比较常见,因为Action的返回值,AspNetCore框架还需要帮我们使用比如json
、xml
等格式序列化,我们不好去修改框架的行为,给它人为加一个using
。
像这种情况,其实微软早就想到,可以让AspNetCore替我们去释放,在HttpContext.Response
中有一个RegisterForDispose方法注册需要Dispose对象,它会在当前Http请求结束时会调用这个方法,用来释放对象。
[ApiController]
public TestController : Controller
{
[HttPost]
PooledList<int> GetSome()
{
var list = BLL.GetSome();
// 注册Dispose, 将在Http请求结束时会帮我们释放list
HttpContext.Response.RegisterForDispose(list);
return list;
}
}
当然也可以用后面要介绍的Dispose.Scope
项目。
介绍Dispose.Scope项目
Dispose.Scope
是一个可以让你方便的使用作用域管理实现了IDisposable
接口的对象实例的类库。它的实现方式和代码都很简单。将需要释放的IDisposable
注册到作用域中,然后在作用域结束时自动释放所有注册的对象。
Github地址:https://github.com/InCerryGit/Dispose.Scope
NuGet包地址:https://www.nuget.org/packages/Dispose.Scope
使用方式
Dispose.Scope
使用非常简单,只需要几步就能完成上文中提到的功能。首先安装Nuget包:
NuGet
Install-Package Dispose.Scope
dotnet add package Dispose.Scope
paket add Dispose.Scope
你可以直接使用Dispose.Scope
的API,本文的所有样例你都可以在samples
文件夹中找到,比如我们有一个类叫NeedDispose
代码如下:
public class NeedDispose : IDisposable
{
public NeedDispose(string name)
{
Name = name;
}
public string Name { get; set; }
public void Dispose()
{
Console.WriteLine("Dispose");
}
}
然后我们就可以像下面这样使用DisposeScope
:
using Dispose.Scope;
using (var scope = DisposeScope.BeginScope())
{
var needDispose = new NeedDisposeClass("A1");
// register to current scope
needDispose.RegisterDisposeScope();
}
// output: A1 Is Dispose
同样,在异步上下文中也可以使用DisposeScope
:
using (var scope = DisposeScope.BeginScope())
{
await Task.Run(() =>
{
var needDispose = new NeedDispose("A2");
// register to current scope
needDispose.RegisterDisposeScope();
});
}
// output: A2 Is Dispose
当然我们可以在一个DisposeScope
的作用域当中,嵌套多个DisposeScope
,如果上下文中存在DisposeScope
那么他们会直接使用上下文中的,如果没有那么他们会创建一个新的。
using (_ = DisposeScope.BeginScope())
{
var d0 = new NeedDispose("D0").RegisterDisposeScope();
using (_ = DisposeScope.BeginScope())
{
var d1 = new NeedDispose("D1").RegisterDisposeScope();
}
using (_ = DisposeScope.BeginScope())
{
var d2 = new NeedDispose("D2").RegisterDisposeScope();
}
}
// output:
// D0 is Dispose
// D1 is Dispose
// D2 is Dispose
如果你想让嵌套的作用域优先释放,那么作用域调用BeginScope
方法时需要指定DisposeScopeOption.RequiresNew
(关于DisposeScopeOption
选项可以查看下面的的内容),它不管上下文中有没有作用域,都会创建一个新的作用域:
using (_ = DisposeScope.BeginScope())
{
var d0 = new NeedDispose("D0").RegisterDisposeScope();
using (_ = DisposeScope.BeginScope(DisposeScopeOption.RequiresNew))
{
var d1 = new NeedDispose("D1").RegisterDisposeScope();
}
using (_ = DisposeScope.BeginScope(DisposeScopeOption.RequiresNew))
{
var d2 = new NeedDispose("D2").RegisterDisposeScope();
}
}
// output:
// D1 Is Dispose
// D2 Is Dispose
// D0 Is Dispose
如果你不想在嵌套作用域中使用DisposeScope
,那么可以指定DisposeScopeOption.Suppress
,它会忽略上下文的DisposeScope
,但是如果你在没有DisposeScope
上下文中使用RegisterDisposeScope
,默认会抛出异常。
using (_ = DisposeScope.BeginScope())
{
var d0 = new NeedDispose("D0").RegisterDisposeScope();
using (_ = DisposeScope.BeginScope(DisposeScopeOption.RequiresNew))
{
var d1 = new NeedDispose("D1").RegisterDisposeScope();
}
using (_ = DisposeScope.BeginScope(DisposeScopeOption.Suppress))
{
// was throw exception, because this context is not DisposeScope
var d2 = new NeedDispose("D2").RegisterDisposeScope();
}
}
// output:
// System.InvalidOperationException: Can not use Register on not DisposeScope context
// at Dispose.Scope.DisposeScope.Register(IDisposable disposable) in E:\MyCode\PooledScope\src\Dispose.Scope\DisposeScope.cs:line 100
// at Program.<<Main>$>g__Method3|0_4() in E:\MyCode\PooledScope\Samples\Sample\Program.cs:line 87
// at Program.<Main>$(String[] args) in E:\MyCode\PooledScope\Samples\Sample\Program.cs:line 9
如果不想让它抛出异常,那么只需要在开始全局设置DisposeScope.ThrowExceptionWhenNotHaveDisposeScope = false
,在没有DisposeScope
的上下文中,也不会抛出异常.
// set false, no exceptions will be thrown
DisposeScope.ThrowExceptionWhenNotHaveDisposeScope = false;
using (_ = DisposeScope.BeginScope())
{
var d0 = new NeedDispose("D0").RegisterDisposeScope();
using (_ = DisposeScope.BeginScope(DisposeScopeOption.RequiresNew))
{
var d1 = new NeedDispose("D1").RegisterDisposeScope();
}
using (_ = DisposeScope.BeginScope(DisposeScopeOption.Suppress))
{
// no exceptions will be thrown
var d2 = new NeedDispose("D2").RegisterDisposeScope();
}
}
// output:
// D1 Is Dispose
// D0 Is Dispose
DisposeScopeOption
枚举 | 描述 |
---|---|
DisposeScopeOption.Required |
作用域内需要 DisposeScope。如果已经存在,它使用环境 DisposeScope。否则,它会在进入作用域之前创建一个新的 DisposeScope。这是默认值。 |
DisposeScopeOption.RequiresNew |
无论环境中是否有 DisposeScope,始终创建一个新的 DisposeScope |
DisposeScopeOption.Suppress |
创建作用域时会抑制环境 DisposeScope 上下文。作用域内的所有操作都是在没有环境 DisposeScope 上下文的情况下完成的。 |
Collections.Pooled扩展
本项目一开始的初衷就是为了更方面的使用Collections.Pooled,它基于官方的System.Collections.Generic
,实现了基于System.Buffers.ArrayPool
的集合对象分配。
基于池的集合对象生成有着非常好的性能和非常低的内存占用。但是您在使用中需要手动为它进行Dispose
,这在单一的方法中还好,有时您会跨多个方法,写起来会比较麻烦,而且有时会忘记去释放它,失去了使用Pool的意义,如下所示:
using Collections.Pooled;
Console.WriteLine(GetTotalAmount());
decimal GetTotalAmount()
{
// forget to dispose `MethodB` result
var result = GetRecordList().Sum(x => x.Amount);
return result;
}
PooledList<Record> GetRecordList()
{
// register to dispose scope
var list = DbContext.Get().ToPooledList();
return list;
}
现在您可以添加Dispose.Scope
的类库,这样可以在外围设置一个Scope
,当方法结束时,作用域内注册的对象都会Dispose
。
using Dispose.Scope;
using Collections.Pooled;
// dispose the scope all registered objects
using(_ = DisposeScope.BeginScope)
{
Console.WriteLine(GetTotalAmount());
}
decimal GetTotalAmount()
{
// forget to dispose `MethodB` result, but don't worries, it will be disposed automatically
var result = GetRecordList().Sum(x => x.Amount);
return result;
}
PooledList<Record> GetRecordList()
{
// register to dispose scope, it will be disposed automatically
var list = DbContext.Get().ToPooledList().RegisterDisposeScope();
// or
var list = DbContext.Get().ToPooledListScope();
return list;
}
性能
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.203
[Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
DefaultJob : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|
GetSomeClassUsePooledUsing | 169.4 ms | 1.60 ms | 1.50 ms | 0.70 | 0.01 | 53333.3333 | 24333.3333 | - | 305 MB |
GetSomeClassUsePooledScope | 169.6 ms | 1.47 ms | 1.30 ms | 0.70 | 0.01 | 53000.0000 | 24333.3333 | - | 306 MB |
GetSomeClass | 240.9 ms | 1.92 ms | 1.60 ms | 1.00 | 0.00 | 112333.3333 | 58000.0000 | 41333.3333 | 632 MB |
GetSomeClassUsePooled | 402.2 ms | 7.78 ms | 8.96 ms | 1.68 | 0.03 | 83000.0000 | 83000.0000 | 83000.0000 | 556 MB |
表格中GetSomeClassUsePooledScope
就是使用Dispose.Scope
的性能,可以看到它基本和手动using
一样,稍微有一点额外的开销就是需要创建DisposeScope
对象。
Asp.Net Core扩展
安装Nuget包Dispose.Scope.AspNetCore
.
NuGet
Install-Package Dispose.Scope.AspNetCore
dotnet add package Dispose.Scope.AspNetCore
paket add Dispose.Scope.AspNetCore
在Asp.Net Core中,返回给Client端是需要Json序列化的集合类型,这种场景下不太好使用Collections.Pooled
,因为你需要在请求处理结束时释放它,但是你不能方便的修改框架中的代码,如下所示:
using Collections.Pooled;
[ApiController]
[Route("api/[controller]")]
public class RecordController : Controller
{
// you can't dispose PooledList<Record>
PooledList<Record> GetRecordList(string id)
{
return RecordDal.Get(id);
}
}
......
public class RecordDal
{
public PooledList<Record> Get(string id)
{
var result = DbContext().Get(r => r.id == id).ToPooledList();
return result;
}
}
现在你可以引用Dispose.Scope.AspNetCore
包,然后将它注册为第一个中间件(其实只要在你使用Pooled类型之前即可),然后使用ToPooledListScope
或者RegisterDisposeScope
方法;这样在框架的求处理结束时,它会自动释放所有注册的对象。
using Dispose.Scope.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
// register UsePooledScopeMiddleware
// it will be create a scope when http request begin, and dispose it when http request end
app.UsePooledScope();
app.MapGet("/", () => "Hello World!");
app.MapControllers();
app.Run();
......
[ApiController]
[Route("api/[controller]")]
public class RecordController : Controller
{
PooledList<Record> GetRecordList(string id)
{
return RecordDal.Get(id);
}
}
......
public class RecordDal
{
public PooledList<Record> Get(string id)
{
// use `ToPooledListScope` to register to dispose scope
// will be dispose automatically when the scope is disposed
var result = DbContext().Get(r => r.id == id).ToPooledListScope();
return result;
}
}
性能
在ASP.NET Core使用了DisposeScope
和PooledList
,也使用普通的List
作为对照组。使用https://github.com/InCerryGit/Dispose.Scope/tree/master/benchmarks
代码进行压测,结果如下:
机器配置
Server:1 Core
Client:5 Core
由于是使用CPU亲和性进行绑核,存在Client抢占Server的Cpu资源的情况,结论仅供参考。
项目 | 总耗时 | 最小耗时 | 平均耗时 | 最大耗时 | QPS | P95延时 | P99延时 | 内存占用率 |
---|---|---|---|---|---|---|---|---|
DisposeScope+PooledList | 1997 | 1 | 9.4 | 80 | 5007 | 19 | 31 | 59MB |
List | 2019 | 1 | 9.5 | 77 | 4900 | 19 | 31 | 110MB |
通过几次平均取值,使用Dispose.Scope
结合PooledList
的场景,内存占用率要低53%,QPS高了2%左右,其它指标基本没有任何性的退步。
注意
在使用Dispose.Scope
需要注意一个场景,那就是在作用域内有跨线程操作时,比如下面的例子:
using Dispose.Scope;
using(var scope = DisposeScope.BeginScope())
{
// do something
_ = Task.Run(() =>
{
// do something
var list = new PooledList<Record>().RegisterDisposeScope();
});
}
上面的代码存在严重的问题,当外层的作用域结束时,可能内部其它线程的任务还未结束,就会导致对象错误的被释放。如果您遇到这样的场景,您应该抑制上下文中的DisposeScope
,然后在其它线程中重新创建作用域。
using Dispose.Scope;
using(var scope = DisposeScope.BeginScope())
{
// suppress context scope
using(var scope2 = DisposeScope.BeginScope(DisposeScopeOption.Suppress))
{
_ = Task.Run(() =>
{
// on other thread create new scope
using(var scope = DisposeScope.BeginScope())
{
// do something
var list = new PooledList<Record>().RegisterDisposeScope();
}
});
}
}
总结
本文对上一篇文章中大家问的比较多的几个问题统一回答了一下,另外就是介绍了一种使用作用域管理Dispose
对象的一个类库。不过也要告诫大家,在采用新的框架和技术之前一定要充分评估,到底应不应该使用这个技术,会带来哪些风险,然后进行详细的测试。
.NET性能优化-推荐使用Collections.Pooled(补充)的更多相关文章
- .NET性能优化-推荐使用Collections.Pooled
简介 性能优化就是如何在保证处理相同数量的请求情况下占用更少的资源,而这个资源一般就是CPU或者内存,当然还有操作系统IO句柄.网络流量.磁盘占用等等.但是绝大多数时候,我们就是在降低CPU和内存的占 ...
- windows10 性能优化
公司的电脑 CPU 是 i5, 内存: 8GB, 机械硬盘, 装的是 win10 操作系统, 作为开发机, 配置本来够低了, 公司又预装了很多个监控软件, 性能就更差了. 这些天明显感觉这个机器越来越 ...
- [推荐]移动H5前端性能优化指南
[推荐]移动H5前端性能优化指南 http://isux.tencent.com/h5-performance.html
- [推荐]T- SQL性能优化详解
[推荐]T- SQL性能优化详解 博客园上一篇好文,T-sql性能优化的 http://www.cnblogs.com/Shaina/archive/2012/04/22/2464576.html
- 推荐:Java性能优化系列集锦
Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难.随着10多年Java平台的改进以及新出现的多核多处理器,Java软件的性能和扩展性已经今非昔比了.现代JVM持续演 ...
- 推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化)解决面试中遇到问题(图解版)
欢迎一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇 > <实战演练,拒绝996篇 > 欢迎关注我博客 也欢迎关注公 众 号[Ccww笔记],原创技术文章 ...
- 25个Apache性能优化技巧推荐
25个Apache性能优化技巧推荐 Apache至今仍处于web服务器领域的霸主,无人撼动,没有开发者不知道.本篇文章介绍25个Apache性能优化的技巧,如果你能理解并掌握,将让你的Apache性能 ...
- 初探性能优化——2个月到4小时的性能提升(copy)推荐阅读
一直不知道性能优化都要做些什么,从哪方面思考,直到最近接手了一个公司的小项目,可谓麻雀虽小五脏俱全.让我这个编程小白学到了很多性能优化的知识,或者说一些思考方式.真的感受到任何一点效率的损失放大一定倍 ...
- IOS 性能优化的建议和技巧
IOS 性能优化的建议和技巧 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelo ...
随机推荐
- webpack的安装 以及 问题 以及 作用
参考链接: https://blog.csdn.net/Rnger/article/details/81086938 https://blog.csdn.net/qq_38111015/art ...
- numpy教程05---ndarray的高级操作
欢迎关注公众号[Python开发实战], 获取更多内容! 工具-numpy numpy是使用Python进行数据科学的基础库.numpy以一个强大的N维数组对象为中心,它还包含有用的线性代数,傅里叶变 ...
- 机器学习---kmeans聚类的python实现
""" Name: study_kmeans.py Author: KX-Lau Time: 2020/11/6 16:59 Desc: 实现kmeans聚类 " ...
- python---希尔排序的实现
def shell_sort(alist): """希尔排序""" n = len(alist) gap = n // 2 # 插入算法执行 ...
- [源码解析] TensorFlow 分布式环境(8) --- 通信机制
[源码解析] TensorFlow 分布式环境(8) --- 通信机制 目录 [源码解析] TensorFlow 分布式环境(8) --- 通信机制 1. 机制 1.1 消息标识符 1.1.1 定义 ...
- 线性表(python实现)
线性表 1 定义 线性表是由 \(n(n>=0)\)个数据元素(节点)\(a1.a2.a3.-.an\) 成的有限序列.该序列中的所有节点都具有相同的数据类型.其中,数据元素的个数 \(n\) ...
- Mybatis-Plus查询整理
1.Hibernate是全ORM(对象关系映射)框架,利用完整的javabean对象与数据库映射结构来自动生成sql. 2.Mybatis是半ORM框,仅有字段映射,需要手写sql语句和对象字段结合生 ...
- 6.Jenkins进阶之流水线pipeline语法入门学习(1)
目录一览: 0x00 前言简述 Pipeline 介绍 Pipeline 基础知识 Pipeline 扩展共享库 BlueOcean 介绍 0x01 Pipeline Syntax (0) Groov ...
- Codeforces Round #741 (Div. 2), problem: (D1) Two Hundred Twenty One (easy version), 1700
Problem - D1 - Codeforces 题意: 给n个符号(+或-), +代表+1, -代表-1, 求最少删去几个点, 使得 题解(仅此个人理解): 1. 这题打眼一看, 肯定和奇 ...
- 2021.07.26 P1022 计算器的改良(字符串)
2021.07.26 P1022 计算器的改良(字符串) 改进: 如果是我出题,我一定把未知数设为ab.buh.bluesky之类的长度不只是1的字符串! 题意: 一个一元一次方程,求解. 分析: 1 ...