高性能对象池实现

内存池用于对频繁申请的内存进行管理进而提升分配效率,但缺乏对一些创建和销毁开销比较大的对象的复用手段,因此对象池应运而生。而当系统中存在大量对象需要频繁创建和销毁时,如何减少大量的耗时开销是对象池构建的关键点之一,本文以此出发,与大家共同探讨高性能对象池的实现。文章作者:杨哲,腾讯WXG后台研发工程师。

一、背景

内存池用于对频繁申请的内存进行管理,通过合理的分配策略和内存布局来减少内存的碎片化以及提高内存的分配效率。但是对于一些创建和销毁开销大的对象,内存池缺乏对这些对象进行复用的手段,因此出现了对象池。

从内存分配的角度来看,相对于内存池,对象池管理的是定长内存,所以无需考虑内存碎片的问题,在内存管理策略上也更加的简单。我们的系统中存在的大量对象需要频繁地创建和销毁,产生了大量的耗时开销,因此需要对象池提供对象复用的方式来避免构造析构产生的开销,或者是通过对象的重置来减少创建销毁对象的开销。

另一方面相对于目前的内存分配器,对象内存的管理更加简单,因此相对于现有的内存分配器在内存分配和释放的效率有一定的提升空间。

二、目标

  • 对象可复用:通过复用对象来避免频繁地调用 malloc 和 free 函数,或者是减少构造析构产生的开销,从而提升性能;

  • 高性能:高性能是设计这个对象池最主要的目标,这里的高性能指的是内存分配和释放的开销足够低;

  • 线程安全:对象池可能会被多个线程同时访问,因此要保证对象池的线程安全;

  • 对象的容量支持动态扩展;

  • 优先分配使用过的对象。

三、方案调研

1. 对象池

(1)brpc object pool

brpc object pool 通过批量分配和归还内存以避免全局竞争,从而降低单次分配、释放的开销。brpc object pool 每个线程的分配流程如下:

  1. 查看 thread-local free block 或者空闲对象数组。如果还有 free 的对象,返回。没有的话步骤2。

  2. 尝试从全局资源池取一块空闲的空间,若取到的话回到步骤1,否则步骤3。

  3. 从全局资源池从系统申请一大块内存,返回其中第一个对象。

释放流程为将对象回收到 thread-local 的空闲对象数组中,攒够数量后回收到全局资源池。

(2)go对象池

Pool 会为每个协程维护一个本地池,本地池分为私有池 private 和共享池 shared。私有池中的元素只能本地协程使用,共享池中的元素可能会被其他协程偷走,所以使用私有池 private 时不用加锁,而使用共享池 shared 时需加锁。

通过对象池获取对象时会优先查找本地 private 池,再查找本地 shared 池,最后查找其他协程的 shared 池,如果以上全部没有可用元素,最后会调用 New 函数获取新元素。详细的分配过程如下图所示:

回收对象时优先把元素放在 private 池中。如果 private 不为空,则放在 shared 池中。

(3)Netty recycler

每个线程都拥有 thread-local 的 Stack, 在 Stack 中维护对象数组以及 WeakOrderQueue 的相关指针。对于全局资源分配机制,当本线程 thread1 回收本线程产生的对象时, 会将对象以 DefaultHandle 的形式存放在 Stack 中。其它线程 thread2 也可以回收 thread1 产生的对象,thread2 回收的对象不会立即放回 thread1 的 Stack 中,而是保存在 thread2 内部的一个 WeakOrderQueue 中。这些外部线程的 WeakOrderQueue 以链表的方式和 Stack 关联起来。

默认情况下一个线程最多持有 2*核数个 WeakOrderQueue,也就是说一个线程最多可以帮 2*核数个外部线程的对象池回收对象。WeakOrderQueue 内部有以 Link 来管理对象。每个 Link 存放的对象是有限的,一个 Link 最多存放16个对象。如果满了则会再产生一个Link 继续存放。

当前线程从对象池中拿对象时, 首先从 Stack中获取,若没有的话,将尝试从 cursor 指向的 WeakOrderQueue 中回收一个 Link 的对象,。如果回收到了就继续从Stack中获取对象;如果没有回收到就创建对象。一个对象池中最多存放 4K 个对象 , Link节点中每个 DefaultHandle 数组默认长度 16,这两个参数可以控制。

2. 内存池

虽然内存池使用的场景和对象池有区别,除了分配的速度外内存池还需要考虑内存碎片的问题,但是内存池在应对多线程访问时的减少锁竞争思路是可以借鉴的。

【文章福利】另外小编还整理了一些C/C++后台开发教学视频,相关面试题,后台学习路线图免费分享,需要的可以自行添加:Q群:720209036 点击加入~ 群文件共享

小编强力推荐C++后台开发免费学习地址:C/C++Linux服务器开发高级架构师/C++后台开发架构师​

(1)tcmalloc

在 tcmalloc 中每个线程都有一个线程局部的 ThreadCache,按照对象的大小进行分类维护对象的链表。如果 ThreadCache 的对象不够了,就从 CentralCache 进行批量分配。如果 CentralCache 没有可分配的对象,就从 PageHeap 申请 Span。如果 PageHeap 没有合适的 Page,就从操作系统申请。

在释放内存的时候,ThreadCache 依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache。CentralCache 发现一个 Span 的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap 发现一批连续的 Page 都释放了,就可以归还给操作系统。

(2)jemalloc

虚拟内存被逻辑上分割成 chunks(默认是4MB,1024个4k页),访问线程通过 round-robin 算法在第一次 malloc 的时候分配 arena,每个 arena 都是相互独立的,维护自己的 chunks, chunk 切割 pages 到 small/large 对象。free() 的内存总是返回到所属的 arena 中,而不管是哪个线程调用 free()。通过 arena 分配的时候需要对 arena bin(每个 small size-class 一个,细粒度)加锁,或 arena 本身加锁。并且

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值