ArrayPool 源码解读之 byte[] 也能池化?
一:背景
1. 讲故事
最近在分析一个 dump 的过程中发现其在 gen2 和 LOH 上有不少size较大的free,仔细看了下,这些free生前大多都是模板引擎生成的html片段的byte[]数组,当然这篇我不是来分析dump的,而是来聊一下,当托管堆有很多length较大的 byte[] 数组时,如何让内存利用更高效,如何让gc老先生压力更小。
不知道大家有没有发现在 .netcore 中增加了不少池化对象的东西,比如: ArrayPool,ObjectPool 等等,确实在某些场景下还是特别实用的,所以有必要对其进行较深入的理解。
二: ArrayPool 源码分析
1. 一图胜千言
在我花了将近一个小时的源码阅读之后,我画了一张 ArrayPool 的池化图,所谓:一图在手,天下我有
。
有了这张图,接下来再聊几个概念并配上相应源码,我觉得应该就差不多了。
2. 池化的架构分级是什么样的?
ArrayPool 是由若干个 Bucket 组成, 而 Bucket 又由若干个 buffer[]
数组组成, 有了这个概念之后,再配一下代码。
public abstract class ArrayPool<T>
{
public static ArrayPool<T> Create()
{
return new ConfigurableArrayPool<T>();
}
}
internal sealed class ConfigurableArrayPool<T> : ArrayPool<T>
{
private sealed class Bucket
{
internal readonly int _bufferLength;
private readonly T[][] _buffers;
private int _index;
}
private readonly Bucket[] _buckets; //bucket数组
}
3. 为什么每一个 bucket 里都有 50 个 buffer[]
这个问题很好回答,初始化时做了 maxArraysPerBucket=50
设定,当然你也可以自定义,具体参考如下代码:
internal sealed class ConfigurableArrayPool<T> : ArrayPool<T>
{
internal ConfigurableArrayPool() : this(1048576, 50)
{
}
internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
{
int num = Utilities.SelectBucketIndex(maxArrayLength);
Bucket[] array = new Bucket[num + 1];
for (int i = 0; i < array.Length; i++)
{
array[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, id);
}
_buckets = array;
}
}
4. bucket 中 buffer[].length 为什么依次是 16,32,64 ...
框架做了默认假定,第一个bucket中的 buffer[].length=16
, 后续 bucket 中的 buffer[].length
都是 x2 累计,涉及到代码就是 GetMaxSizeForBucket()
方法,参考如下:
internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
{
Bucket[] array = new Bucket[num + 1];
for (int i = 0; i < array.Length; i++)
{
array[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, id);
}
}
internal static int GetMaxSizeForBucket(int binIndex)
{
return 16 << binIndex;
}
5. 初始化时 bucket 到底有多少个?
其实在上图中我也没有给出 bucket 到底有多少个,那到底是多少个呢???? ,当我阅读完源码之后,这算法还挺有意思的。
先说一下结果吧,默认 17 个 bucket,你肯定会好奇怎么算的? 先说下两个变量:
-
maxArrayLength=1048576 = 2的20次方
-
buffer.length= 16 = 2的4次方
最后的算法就是取次方的差值:bucket[].length= 20 - 4 + 1 = 17
,换句话说最后一个 bucket 下的 buffer[].length=1048576
,详细代码请参考 SelectBucketIndex()
方法。
internal sealed class ConfigurableArrayPool<T> : ArrayPool<T>
{
internal ConfigurableArrayPool(): this(1048576, 50)
{ }
internal ConfigurableArrayPool(int maxArrayLength, int maxArraysPerBucket)
{
int num = Utilities.SelectBucketIndex(maxArrayLength);
Bucket[] array = new Bucket[num + 1];
for (int i = 0; i < array.Length; i++)
{
array[i] = new Bucket(Utilities.GetMaxSizeForBucket(i), maxArraysPerBucket, id);
}
_buckets = array;
}
internal static int SelectBucketIndex(int bufferSize)
{
return BitOperations.Log2((uint)(bufferSize - 1) | 0xFu) - 3;
}
}
到这里我相信你对 ArrayPool 的池化架构思路已经搞明白了,接下来看下如何申请和归还 buffer[]。
三:如何申请和归还
既然 buffer[] 做了颗粒化,那就应该好借好还,反应到代码上就是 Rent()
和 Return()
方法,为了方便理解,上代码说话:
class Program
{
static void Main(string[] args)
{
var arrayPool = ArrayPool<int>.Create();
var bytes = arrayPool.Rent(10);
for (int i = 0; i < bytes.Length; i++) bytes[i] = 10;
arrayPool.Return(bytes);
Console.ReadLine();
}
}
有了代码和图之后,再稍微捋一下流程。
- 从 ArrayPool 中借一个
byte[10]
大小的数组,为了节省内存,先不备货,临时生成一个byte[].size=16
的数组出来,简化后的代码如下,参考if (flag)
处:
internal T[] Rent()
{
T[][] buffers = _buffers;
T[] array = null;
bool lockTaken = false;
bool flag = false;
try
{
if (_index < buffers.Length)
{
array = buffers[_index];
buffers[_index++] = null;
flag = array == null;
}
}
if (flag)
{
array = new T[_bufferLength];
}
return array;
}
这里有一个坑,那就是你以为借了 byte[10]
,现实给你的是 byte[16]
,这里稍微注意一下。
- 当用 ArrayPool.Return 归还
byte[16]
时, 很明显看到它落到了第一个bucket的第一个buffer[]上,参考如下简化后的代码:
internal void Return(T[] array)
{
if (_index != 0)
{
_buffers[--_index] = array;
}
}
这里也有一个值得注意的坑,那就是还回去的 byte[16]
里面的数据默认是不会清掉的,从上面的代码也是可以看出来的,要想做清理,需要在 Return 方法中指定 clearArray=true
,参考如下代码:
public override void Return(T[] array, bool clearArray = false)
{
int num = Utilities.SelectBucketIndex(array.Length);
if (num < _buckets.Length)
{
if (clearArray)
{
Array.Clear(array, 0, array.Length);
}
_buckets[num].Return(array);
}
}
四:总结
学习这其中的 池化架构
思想,对平时项目开发还是能提供一些灵感的,其次对那些一次性使用 byte[]
的场景,用池化是个非常不错的方法,这也是我对朋友dump分析后提出的一个优化思路。
更多高质量干货:参见我的 GitHub: dotnetfly
![图片名称](https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg)
相关文章
- NETs相关基因构建预后模型干湿结合发12分+SCI
- 用 Minio 快速启动 Velero 实现 Kubernetes资源备份
- MySQL(1) - 用户管理及root密码修改
- Jmeter系列(40)- 详解 Jmeter 图形化 HTML 压测报告
- Jmeter系列(39)- 详解 Jmeter CLI 模式
- Jmeter系列(38)- Jmeter 分布式测试
- Jmeter系列(37)- 跨平台运行 Jmeter,CSV 文件路径设置
- Jenkins操作手册 - 巨详细,一篇足矣!
- 【JVM实战系列】「监控调优体系」实战开发arthas-spring-boot-starter监控你的微服务是否健康
- 【Spring专题】「开发指南」夯实实战基础功底之解读logback-spring.xml文件的详解实现
- 精华推荐 |【深入浅出Sentinel原理及实战】「原理探索专题」完整剖析Alibaba微服务架构体系之轻量级高可用流量控制组件Sentinel(1)
- 【深入浅出Spring原理及实战】「源码原理实战」从底层角度去分析研究PropertySourcesPlaceholderConfigurer的原理及实战注入机制
- 精华推荐 | 【深入浅出RocketMQ原理及实战】「性能原理挖掘系列」透彻剖析贯穿RocketMQ的事务性消息的底层原理并在分析其实际开发场景
- 深度剖析 | 【JVM深层系列】[HotSpotVM研究系列] JVM调优的"标准参数"的各种陷阱和坑点分析(攻克盲点及混淆点)「 1 」
- 【JVM故障问题排查心得】「内存诊断系列」Docker容器经常被kill掉,k8s中该节点的pod也被驱赶,怎么分析?
- 【深入浅出SpringCloud原理及实战】「SpringCloud-Alibaba系列」微服务模式搭建系统基础架构实战指南及版本规划踩坑分析
- 【秒杀购物商城业务服务】「分布式架构服务」盘点中间件服务的高可用模式及集群技术的方案分析
- 作者推荐 | 【分布式技术专题】「架构设计方案」图解学习法总结集群模式下的各种软负载均衡策略实现及原理分析
- 【分布式技术专题】「架构设计方案」盘点和总结秒杀服务的功能设计及注意事项技术体系
- Kafka技术专题之「性能调优篇」消息队列服务端出现内存溢出OOM以及相关性能调优实战分析