简介

在上一篇.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框架还需要帮我们使用比如jsonxml等格式序列化,我们不好去修改框架的行为,给它人为加一个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使用了DisposeScopePooledList,也使用普通的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(补充)的更多相关文章

  1. .NET性能优化-推荐使用Collections.Pooled

    简介 性能优化就是如何在保证处理相同数量的请求情况下占用更少的资源,而这个资源一般就是CPU或者内存,当然还有操作系统IO句柄.网络流量.磁盘占用等等.但是绝大多数时候,我们就是在降低CPU和内存的占 ...

  2. windows10 性能优化

    公司的电脑 CPU 是 i5, 内存: 8GB, 机械硬盘, 装的是 win10 操作系统, 作为开发机, 配置本来够低了, 公司又预装了很多个监控软件, 性能就更差了. 这些天明显感觉这个机器越来越 ...

  3. [推荐]移动H5前端性能优化指南

    [推荐]移动H5前端性能优化指南 http://isux.tencent.com/h5-performance.html

  4. [推荐]T- SQL性能优化详解

    [推荐]T- SQL性能优化详解 博客园上一篇好文,T-sql性能优化的 http://www.cnblogs.com/Shaina/archive/2012/04/22/2464576.html

  5. 推荐:Java性能优化系列集锦

    Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难.随着10多年Java平台的改进以及新出现的多核多处理器,Java软件的性能和扩展性已经今非昔比了.现代JVM持续演 ...

  6. 推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化)解决面试中遇到问题(图解版)

    欢迎一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇 > <实战演练,拒绝996篇 > 欢迎关注我博客 也欢迎关注公 众 号[Ccww笔记],原创技术文章 ...

  7. 25个Apache性能优化技巧推荐

    25个Apache性能优化技巧推荐 Apache至今仍处于web服务器领域的霸主,无人撼动,没有开发者不知道.本篇文章介绍25个Apache性能优化的技巧,如果你能理解并掌握,将让你的Apache性能 ...

  8. 初探性能优化——2个月到4小时的性能提升(copy)推荐阅读

    一直不知道性能优化都要做些什么,从哪方面思考,直到最近接手了一个公司的小项目,可谓麻雀虽小五脏俱全.让我这个编程小白学到了很多性能优化的知识,或者说一些思考方式.真的感受到任何一点效率的损失放大一定倍 ...

  9. IOS 性能优化的建议和技巧

    IOS 性能优化的建议和技巧 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelo ...

随机推荐

  1. CCF201512-2消除类游戏

    问题描述 消除类游戏是深受大众欢迎的一种游戏,游戏在一个包含有n行m列的游戏棋盘上进行,棋盘的每一行每一列的方格上放着一个有颜色的棋子,当一行或一列上有连续三个或更多的相同颜色的棋子时,这些棋子都被消 ...

  2. 【深度学习 论文篇 01-1 】AlexNet论文翻译

    前言:本文是我对照原论文逐字逐句翻译而来,英文水平有限,不影响阅读即可.翻译论文的确能很大程度加深我们对文章的理解,但太过耗时,不建议采用.我翻译的另一个目的就是想重拾英文,所以就硬着头皮啃了.本文只 ...

  3. vue3 监听路由($route)变化

      setup() {      // ...   },   watch: {     $route(m, n) {       console.log('mm', m)       console. ...

  4. go context详解

    Context通常被称为上下文,在go中,理解为goroutine的运行状态.现场,存在上下层goroutine context的传递,上层goroutine会把context传递给下层gorouti ...

  5. 可怕!CPU暗藏了这些未公开的指令!

    大家好,我是轩辕. 我们知道,我们平时编程写的高级语言,是经过编译器编译以后,变成了CPU可以执行的机器指令: 而CPU能支持的指令,都在它的指令集里面了. 很久以来,我都在思考一个问题: CPU有没 ...

  6. Nginx下载文件指定文件名称

    配置 location ^~/TEMP/ { alias/share/files/; if ($request_uri ~* ^.*\/(.*)\.(txt|doc|pdf|rar|gz|zip|do ...

  7. JuiceFS 缓存预热详解

    缓存预热是一个比较常见的概念,相信很多小伙伴都有所了解.对于 JuiceFS 来说,缓存预热就是将需要操作的数据预先从对象存储拉取到本地,从而获得与使用本地存储类似的性能表现. 缓存预热 JuiceF ...

  8. JavaScript の querySelector 使用说明

    本文记录,JavaScript 中 querySelector 的使用方法.小白贡献,语失莫怪. // 两种 query 的 method (方法) document.querySelector(se ...

  9. LCA的离线快速求法

    最常见的LCA(树上公共祖先)都是在线算法,往往带了一个log.有一种办法是转化为"+-1最值问题"得到O(n)+O(1)的复杂度,但是原理复杂,常数大.今天介绍一种允许离线时接近 ...

  10. Redis 内存满了怎么办?这样设置才正确!

    上回在<Redis 数据过期了会被立马删除么?>说到如果过期的数据太多,定时删除无法删除完全(每次删除完过期的 key 还是超过 25%),同时这些 key 再也不会被客户端请求,就无法走 ...