目录
1. TCEntry tc_slots_[kMaxNumTransferEntries]
2. PageMapCache pagemap_cache_
TCMalloc是专门对多线并发的内存管理而设计的,TCMalloc主要是在线程级实现了缓存,使得用户在申请内存时大多情况下是无锁内存分配。整个 TCMalloc对小内存(小于等于256k)的管理实现了三级缓存,分别是ThreadCache(线程级缓存),Central Cache(中央缓存:CentralFreeeList),PageHeap(页缓存)。小内存的分配和释放流程如下图所示,红线表示内存的申请,蓝线表示内存的释放过程。下面将分别介绍各级缓存模块的实现。
一. SizeMap
在介绍三个缓存模块之前,先需要介绍一下sizeMap。
TCMalloc为了提高内存分配效率和减少内存的浪费,对小内存进行了细化分类,在默认的情况下:
size在(0, 16)之间时,以8字节对齐分配内存,size在[16,128)之间,按16字节对齐来分配内存,size在[128,256*1024),按(2^(n+1)-2^n)/8字节对齐来分配内存(n的值为log2(size)取整,见函数AlignmentForSize())。
TCMalloc对这些细化分类构建了两个映射表,即class_array_[kClassArraySize]和class_to_size_[kNumClasses]。 class_array_[kClassArraySize]表示了size到class的映射关系(size需要先经过函数ClassIndex(size) 转换 ), class_to_size_[kNumClasses]表示了class到size的映射关系。要申请一个size的内存时,先从class_array_[ClassIndex(size)]查到size对应的sizeclass,然后从映射表class_to_size_[kNumClasses]获取实际获取的内存大小。
同时TCMalloc另外维护了两种映射表:class_to_pages_[kNumClasses]和num_objects_to_move_[kNumClasses]。class_to_pages_[kNumClasses]表示了Central Cache每次从PageHeap获取内存时,对应的sizeclass每次需要从PageHeap获取几页内存;num_objects_to_move_[kNumClasses]表示了ThreadCache每次从Central Cache获取内存时,对应的sizeclass每次需要从Central Cache获取的buffer(Object)个数。
下面整个表表示了以8k为一页时,上述映射表的内容(由于class_array_[kClassArraySize]比较大,这里就不罗列了)。
sizeclass | class_to_size_ | class_to_pages_ | num_objects_to_move_ |
0 | 0 | 0 | 0 |
1 | 8 | 2 | 8192 |
2 | 16 | 2 | 4096 |
3 | 32 | 2 | 2048 |
4 | 48 | 2 | 1365 |
5 | 64 | 2 | 1024 |
6 | 80 | 2 | 819 |
7 | 96 | 2 | 682 |
8 | 112 | 2 | 585 |
9 | 128 | 2 | 512 |
10 | 144 | 2 | 455 |
11 | 160 | 2 | 409 |
12 | 176 | 2 | 372 |
13 | 192 | 2 | 341 |
14 | 208 | 2 | 315 |
15 | 224 | 2 | 292 |
16 | 240 | 2 | 273 |
17 | 256 | 2 | 256 |
18 | 288 | 2 | 227 |
19 | 320 | 2 | 204 |
20 | 352 | 2 | 186 |
21 | 384 | 2 | 170 |
22 | 416 | 2 | 157 |
23 | 448 | 2 | 146 |
24 | 480 | 2 | 136 |
25 | 512 | 2 | 128 |
26 | 576 | 2 | 113 |
27 | 640 | 2 | 102 |
28 | 704 | 2 | 93 |
29 | 768 | 2 | 85 |
30 | 832 | 2 | 78 |
31 | 896 | 2 | 73 |
32 | 960 | 2 | 68 |
33 | 1024 | 2 | 64 |
34 | 1152 | 2 | 56 |
35 | 1280 | 2 | 51 |
36 | 1408 | 2 | 46 |
37 | 1536 | 2 | 42 |
38 | 1792 | 2 | 36 |
39 | 2048 | 2 | 32 |
40 | 2304 | 2 | 28 |
41 | 2560 | 2 | 25 |
42 | 2816 | 3 | 23 |
43 | 3072 | 2 | 21 |
44 | 3328 | 3 | 19 |
45 | 4096 | 2 | 16 |
46 | 4608 | 3 | 14 |
47 | 5120 | 2 | 12 |
48 | 6144 | 3 | 10 |
49 | 6656 | 5 | 9 |
50 | 8192 | 2 | 8 |
51 | 9216 | 5 | 7 |
52 | 10240 | 4 | 6 |
53 | 12288 | 3 | 5 |
54 | 13312 | 5 | 4 |
55 | 16384 | 2 | 4 |
56 | 20480 | 5 | 3 |
57 | 24576 | 3 | 2 |
58 | 26624 | 7 | 2 |
59 | 32768 | 4 | 2 |
60 | 40960 | 5 | 2 |
61 | 49152 | 6 | 2 |
62 | 57344 | 7 | 2 |
63 | 65536 | 8 | 2 |
64 | 73728 | 9 | 2 |
65 | 81920 | 10 | 2 |
66 | 90112 | 11 | 2 |
67 | 98304 | 12 | 2 |
68 | 106496 | 13 | 2 |
69 | 114688 | 14 | 2 |
70 | 122880 | 15 | 2 |
71 | 131072 | 16 | 2 |
72 | 139264 | 17 | 2 |
73 | 147456 | 18 | 2 |
74 | 155648 | 19 | 2 |
75 | 163840 | 20 | 2 |
76 | 172032 | 21 | 2 |
77 | 180224 | 22 | 2 |
78 | 188416 | 23 | 2 |
79 | 196608 | 24 | 2 |
80 | 204800 | 25 | 2 |
81 | 212992 | 26 | 2 |
82 | 221184 | 27 | 2 |
83 | 229376 | 28 | 2 |
84 | 237568 | 29 | 2 |
85 | 245760 | 30 | 2 |
86 | 253952 | 31 | 2 |
87 | 262144 | 32 | 2 |
二. 线程缓存ThreadCache
ThreadCache是线程缓存的对象,每个线程都有一个ThreadCache对象作为本线程的内存缓存池,当线程需要申请内存时,就从自己的ThreadCache中获取。所有线程的ThreadCache通过双向链表(next_,prev_)连接起来,链表头是静态变量ThreadCache::thread_heaps_ 。
线程对本线程的ThreadCache的访问采用线程私有数据的接口进行访问,线程的私有数据有两种实现方式:
1. 静态局部缓存线程私有数据,通过关键字static __thread定义一个静态变量,这个在TCMalloc由编译宏HAVE_TLS来控制;
2. 动态线程私有数据,通过POSIX接口 pthread_key_create,pthread_setspecific,pthread_getspecific来实现。
静态局部缓存的优点是设置和读取的速度非常快,比动态方式快很多,但是也有它的缺点。
主要有如下两个缺点:
1. 静态缓存在线程结束时没有办法清除;而动态线程私有数据在创建key时,就可以注册释放线程数据的接口(TCMalloc注册的接口DestroyThreadCache),当线程退出时会调用这个接口对线程的私有数据进行清理,即可以释放掉线程申请的资源。
2. 不是所有的操作系统都支持。
tcmalloc采用的是动态局部缓存,但同时检测系统是否支持静态方式,如果支持那么同时保存一份拷贝,方便快速读取。
ThreadCache的实现
ThreadCache的实现比较简单,如上图所示,在ThreadCache的对象中保存一个FreeList list_[kNumClasses]数组,数组的每个元素为FreeList,用于管理某个sizeclass的所有缓存,TCMalloc没有为这些Object设计专用的链表来管理,而是将Buffer头上的4字节或8字节(根据系统而定)用于保存下一个Object的起始地址。链表的结构如下图。
1. 内存分配
TCMalloc分配内存的接口是void* tc_malloc(size_t size);TCMalloc分配内存比较简单,当一个线程需要一块size大小的内存时:
1). 线程调用POXIS的线程私有数据接口获取自己的ThreadCache对象(如果不存在,则创建一个并和线程私有数据的key绑定);
2). 根据SizeMap中的映射表计算出size对应的sizeclass;
3). 根据sizeclass中 list_[kNumClasses]对应的链表中查找是否有Object存在,如果有则将头部的第一个从链表中取出返回给用户;
4). 如果没有,则从sizeclass对应的Central Cache的CentralFreeList中申请一定数量的Object插入到ThreadCache对应的链表中,并将第一个Object返回给用户。
TCMalloc为了提高内存分配的效率,一次从Central Cache申请一定数量的Object到ThreadCache中,一次申请的数量由映射表num_objects_to_move_[kNumClasses]确定。
2. 内存释放
TCMalloc释放内存的接口是void tc_free(void* ptr);接口的参数是释放内存的首地址:
1). 线程调用POXIS的线程私有数据接口获取自己的ThreadCache对象;
2). 首先,会计算出ptr在系统内存的哪页,即PageID(PageID = (ptr) >> kPageShift);
3). 然后调用PageHeap的接口GetSizeClassIfCached(PageID p)从映射表pagemap_cache_得到该页被哪个sizeclass的所使用的(因为TCMalloc申请缓存时是按页来申请的),pagemap_cache_映射表会在PageHeap章节描述;
4). 将ptr放到了ThreadCache的 list_[kNumClasses]对应的链表头部;
5). 如果ptr对应的sizeclass的链表长度已经超过了链表设定的最大长度(最大长度在运行过程中会稍微变化,最好是num_objects_to_move_[kNumClasses]中设定的整数倍),则将num_objects_to_move_[kNumClasses]个Object归还给对应的Central Cache。
6). 如果整个ThreadCache缓存的内存(ThreadCache::size_)大于本ThreadCache设定的最大缓存(ThreadCache::max_size_,max_size_在运行中根据缓存使用也是可以变化的,即可以减少其他ThreadCache的最大缓存,增加自己的最大缓存,见函数IncreaseCacheLimitLocked())时,即启动内存回收机制,释放每个sizeclass链表的一半(lowater_/2)的Objects返回给Central Cache。注意,返还的时候,如果Objects链表长度大于num_objects_to_move_[kNumClasses],则分多次,每次都是num_objects_to_move_[kNumClasses]个Objects的方式返还给Central Cache,原因是Central Cache有两个地方缓存Buffer,会根据返回Objects的数量存放不同的地方,具体分析在Central Cache的实现章节描述。
三. 中央缓存Central Cache
Cen tral Cache是所有线程共享的缓冲区,因此对Central Cache的访问需要加锁。
Central Cache所有的数据都在Static::central_cache_[kNumClasses]中,即采用数组的方式来管理所有的Cache,每个sizeclass的Central Cache对应一个数组元素,所以对不同sizeclass的的Central Cache的访问是不冲突的。对Cache挂到管理主要是有类CentralFreeList来实现,数据结构如下图所示。
CentralFreeList的实现
CentralFreeList是用来在ThreadCache和PageHeap之间缓存Buffer的地方,CentralFreeList有两个地方来缓存Buffer,它们分别是 TCEntry tc_slots_[kMaxNumTransferEntries]和 Span nonempty_。
1. TCEntry tc_slots_[kMaxNumTransferEntries]
tc_slots_[kMaxNumTransferEntries]是用来缓存那些从ThreadCache返还Buffer,只有一次返还num_objects_to_move_[kNumClasses]个Object的那些缓存保存在 tc_slots_[kMaxNumTransferEntries],因此TCEntry链表的长度都是num_objects_to_move_[kNumClasses],这也是为什么前面描述的ThreadCache返回Object长度大于num_objects_to_move_[kNumClasses],则分多次,每次都是于num_objects_to_move_[kNumClasses]个。TCEntry是链表的头,记录了链表的头尾。这样在ThreadCache和CentralFreeList就能快速的移动Object,每次从ThreadCache返回Object给CentralFreeList时,直接将它挂到TCEntry上,ThreadCache从CentralFreeList申请内存也类似,可以将链表直接插入到ThreadCache的链表头。 ThreadCache每次申请内存时首先从 tc_slots_[kMaxNumTransferEntries]中找,如果有就直接从 tc_slots_[kMaxNumTransferEntries]获取,否则采用才从 nonempty_管理的Span中获取。
2. Span nonempty_
Span nonempty_ 主要充当CentralFreeList从PageHeap获取Buffer的缓存,当CentralFreeList的缓存不够时,CentralFreeList从PageHeap申请一定量的Buffer(一次获取的页数有映射表class_to_pages_[kNumClasses]来确定),先缓存在Span nonempty_ 中,然后ThreadCache在到Span nonempty_ 中获取。当ThreadCache一次返还的Object不是num_objects_to_move_[kNumClasses]个或者tc_slots_[kMaxNumTransferEntries]满时,才会将Object缓存到Span nonempty_中。
3. Span empty_
Spen empty_主要是用来保存那些Span下面不再有Objects的Span节点。
4. Span
Span是用于管理连续的内存页,如下图所示,Page1和Page2属于Span a,Page3, Page4, Page5和Page6属于Span b,等等。这些映射关系维护在PageHeap的PageMap pagemap_中,关于PageMap pagemap_将在PageHeap中描述。
需要注意的是Span在PageHeap和CentralFreeList是不同的。
在PageHeap中的Span只是对它管理的连续内存的第一页和最后一页在PageMap pagemap_进行登记,而且在PageHeap中的Span没有对Object进行管理。
而在CentralFreeList中的Span,它会将它管理的所有的页都在PageMap pagemap_进行注册,这时因为当ThreadCache归还内存时是按Object来返回的,从Object只能找到它所在的页,然后才能找到它所归属的Span,因此需要将Span管理所有的页都进行注册。当CentralFreeList从PageHeap申请了一个Span后,还会把Span管理的页划分成本CentralFreeList对应的size的Objects,并且用链表的方式管理起来,链表和ThreadCache中介绍的链表一样。
5. CentralFreeList内存分配
1). ThreadCache向Central Cache申请内存,Central Cache根据sizeclass选择一个CentralFreeList;
2). CentralFreeList首先查看 tc_slots_[kMaxNumTransferEntries] 中是否还有未使用的空闲内存,有则直接返回给ThreadCache(即图中的红线1);
3). 否则从Span nonempty_ 中获取空闲内存(即图中的红线2),如果对应的Span下的Objects分配完了,则将Span移到Spen empty_中;
4). 如果 Span nonempty_ 也没有空闲内存,则从PageHeap中申请一定数量页的内存(页的数量由class_to_pages_[kNumClasses]确定)放到Span nonempty_ 中,同时会将获取的页在PageMap pagemap_中注册(接口是RegisterSizeClass),并且在pagemap_cache_中注册每页的sizeclass(接口是CacheSizeClass()),另外还会对申请到的大块内存划分成本CentralFreeList对应的size的Objects。然后CentralFreeList再从Span nonempty_中获取空闲内存。
6. CentralFreeList内存释放
1): 当ThreadCache释放内存给Central Cache时,Central Cache根据sizeclass选择相应的CentralFreeList;
2): 如果释放的Object的数量正好等于映射表num_objects_to_move_[kNumClasses]中本CentralFreeList的sizeclass对应的数量, 并且 tc_slots_[kMaxNumTransferEntries] 还有空闲的节点, 则将释放的Objects链表挂载tc_slots_[kMaxNumTransferEntries] 的某个节点下(图中蓝线1)。如果没有空闲节点了, 则将内存返给Span nonempty_。 TCMalloc在这里做了一点优化, 如果本CentralFreeList设置的tc_slots_[kMaxNumTransferEntries]中当前的slots(即CentralFreeList::cache_size_), 但还没有达到最大值(即CentralFreeList::max_cache_size_), 则可以减少其它CentralFreeList的slots,而增加自己的slots。
3): 返回给Span nonempty_ 的Objects是一个Object一个Object返回的,因为从ThreadCache返回的所有Objects不一定是属于同一个Span的,而Span管理的Object必须是从原来从PageHeap中申请的连续页的内存,所以原来的Objects从哪个Span申请,必须返回到哪个Span。
4): 如果Span原来管理的所有的Objects都返回到了Span中(span->refcount == 0)[注意: 最后一个Object不需要插入链表,因为PageHeap关心整块内存,不关心Object],即没有被central cache中的tc_slots_[]缓存 或Thread cache使用,则需要将这个Span管理的内存归还给PageHeap。
四. PageHeap
PageHeap在TCMalloc中主要作为Central Cache和操作系统之间的内存缓存和大块内存的申请和释放。PageHeap对内存的管理是通过Span来管理的,关于Span的描述见Central Cache部分。
在介绍PageHeap的整个结构之前,需要先介绍两个映射表,它们分别是PageMap pagemap_和PageMapCache pagemap_cache_。
1. PageMap pagemap_
pagemap_是作为PageID(即页ID)和它归属的Span之间的映射表,TCMalloc为32位系统和64为系统设计不同数据结构,32位系统使用二级的Radix Tree(TCMalloc_PageMap2),而64位系统使用三级Radix Tree(TCMalloc_PageMap3)。对不同的系统采用不同的数据结构主要是考虑这个映射表占用内存的大小。
下图是32为系统,8K页(kPageShift=13)的pagemap_,8K页总共有2^19页(32-13),将高5bits作为root[],而低14位作为Leaf[]。
在当前的__x86_64__处理器中,只用了地址的低48bits用于虚拟和物理地址的转换,高16bits是不用的。所以在8K页的配置下,总共有2^35页(48-13),TCMalloc将35bits分为12,12,11三级Radix Tree,如下图所示。
上面数据结构中的Node和Leaf只有在需要的时候才创建,因此pagemap_实际占用的内存比较少。
2. PageMapCache pagemap_cache_
pagemap_cache_是作为PageID和sizeclass的映射表,即当CentralFreeList从PageHeap获取一页时,就需要将这页的PageID和CentralFreeList所属的sizeclass在这个表中进行注册。 pagemap_cache_实际上是个哈希数组,对于默认kHashbits = 16(即哈希key占用的比特位为16bits, 2^16 = 65536,),kValuebits = 7(即value占用7bits),8k页来说(PageID最大值为19bits),哈希的结构如下所示,哈希的key为PageID的低16bits最为数组的下标,即哈希数组的大小为65536,对将PageID的高3bits和sizeclass(占低7bits)进行位的组合作为哈希的value。
3. PageHeap的实现
PageHeap主要作为Central Cache和操作系统之间的内存缓存和大块内存的申请和释放。PageHeap有两块缓存,一个是用于管理内存小于等于1M的连续页内存,即 SpanList free_[kMaxPages],另一个是那些内存大于1M的连续页内存,即 SpanList large_。
SpanList free_[kMaxPages] 数组是按页大小递增的,即free_[1]是存放管理1页的Span,free_[2]是存放管理2页的Span,依此类推。SpanList结构下面有两个Span链表,这两个链表作用是不同的,Span normal存放的那些还没有释放给系统的Span,而Span returned则存放的是那些已经释放给系统的Span。
{需要注意的是,TCMalloc调用内存释放的接口TCMalloc_SystemRelease,而对应的系统调用的接口是madvise(),建议系统的行为是MADV_FREE,而MADV_FREE则将这些页标识为延迟回收。当内核内存紧张时,这些页将会被优先回收,如果应用程序在页回收后又再次访问,内核将会返回一个新的并设置为0的页。而如果内核内 存充裕时,标识为MADV_FREE的页会仍然存在,后续的访问会清掉延迟释放的标志位并正常读 取原来的数据,因此应用程序不检查页的数据,就无法知道页的数据是否已经被丢弃。
因为 Linux 不支持 MADV_FREE,所以使用了 MADV_DONTNEED。使用 MADV_DONTNEED调用 madvise,告诉内核这段内存今后"很可能"用不到了,其映射的物理内存尽管拿去好了,因此,TCMalloc_SystemRelease 只是告诉内核,物理内存可以回收以做它用,但虚拟空间还留着,下次访问是时会产生缺页中断,而重新申请物理内存。
因此Span returned队列中的内存还是可以重新被上层模块申请使用的。}
4. PageHeap内存分配
1): 当CentralFreeList向PageHeap申请n页内存时(接口是PageHeap::New(Length n)),PageHeap首先在free_[n].normal的队列中查找,如果找到则返回,否则到free_[n].returned查找,如果找到则返回,否则在free_[n+1]中以相同的方法查找。
2): 在大于n的队列中找到(假设在大小为m页的队列中找到),即将这块内存分成两块,分别是n和(m-n),将含n页的Span返回给CentralFreeList,而将含有(m-n)页的Span插入(m-n)页的SpanList中,插入过程中,还要检查插入的(m-n)页的左右相邻页是否也在这个SpanList中存在,如果存在,则将它们合并,合并后则需要找新的SpanList插入,重复这个过程;
3): 如果在 SpanList free_[kMaxPages] 中找不到合适的页,则在SpanList large_中查找,查找过程和SpanList free_[kMaxPages]类似,即在large_.normal和large_.returned中查找最合适的Span。
4): 如果在上述的SpanList free_[kMaxPages]和SpanList large_中都找不到合适的Span,并且PageHeap中还有大量的空闲页,说明在PageHeap中存在大量的内存碎片,则将Span进行尽可能的合并。然后再从SpanList free_[kMaxPages]或SpanList large_查找合适的页的Span。
5): 如果上述都找不到合适的Span,则从系统申请内存来扩充PageHeap(接口: PageHeap::GrowHeap(Length n)),然后再从PageHeap获取内存。
5. PageHeap内存释放
1): 当CentralFreeList的某个Span所管理的内存都已经返回给这个Span后(有Span->refcount指示),CentralFreeList就将相应的Span管理的内存归还给PageHeap(接口: PageHeap::Delete(Span* span))。
2): PageHeap会将这个Span和 free_[n].normal或larg_.normal中的Span进行合并(前提是这个Span管理的页的前页或后页在相应的Span链表中)。如果aggressive_decommit_为TRUE(表示每次回到PageHeap的内存都要归还给系统),则也可以和free_[n].returned或larg_.returned合并,并且都归还给系统(系统会将物理内存收回作为他用,虚拟进程中的虚拟内存还是存在的)。
3): 查看是否需要向系统释放内存,如果需要,则以Round Robin的方式将某个SpanList的尾部的Span释放给系统。释放内存是根据配置和PageHeap中累积的Page数量来执行的,具体的算法见函数PageHeap::IncrementalScavenge(Length n)。
6. 大块内存的分配和释放
TCMalloc对那些一次申请大于256K内存就不在经过ThreadCache,而是直接从PageHeap中分配,分配的接口是do_malloc_pages(ThreadCache* heap, size_t size),对应的PageHeap的接口是PageHeap::New(Length n),这个接口在PageHeap内存分配中已经介绍过了,这里不在累赘了,需要注意的是,通过do_malloc_pages(ThreadCache* heap, size_t size)这个接口获取的内存,需要将第一个的PageID在 pagemap_cache_中将对应的sizeclass置为0,这样在释放内存的时候就知道是大内存,就可以直接调用释放内存的接口PageHeap::Delete(Span* span)将内存直接返还给PageHeap。
五.推荐阅读
《TCMalloc:线程缓存Malloc以及tcmalloc与ptmalloc性能对比》
《ptmalloc、tcmalloc与jemalloc内存分配器对比分析》