昨天聊的64MB内存中,有使用TCMalloc,但是对其原理不是很了解,所以学习了下。
前言
先不使用TCMalloc,先使用glibc自带的内存分配ptmalloc2进行内存分配,先看一个简单的程序:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main( int argc, char *argv[] )
{malloc(1);getchar();
}
编译运行:
gcc -g m.c -o m
./m
查看下我们申请一个字节的内存占用情况:
[root@izbp14xswj2tx6qgnz9dllz ~]# cat /proc/2888/maps|grep heap
00ed3000-00ef4000 rw-p 00000000 00:00 0 [heap]
[root@izbp14xswj2tx6qgnz9dllz ~]# python
Python 2.7.5 (default, Aug 4 2017, 00:39:18)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x00ef4000-0x00ed3000
135168
>>> 135168/1024
132
结论我们申请1个字节的内存,实际内存分配器给我们分配了132KB的内存,这样做出是下次再申请内存的时候,如果小于132KB内存,不用再向系统申请,加快内存分配速度。即时我们把代码改成如下:
char * p = (char*) malloc(1);
free(p);
申请的内存仍然不释放:
[root@izbp14xswj2tx6qgnz9dllz ~]# cat /proc/3248/maps|grep heap
022ce000-022ef000 rw-p 00000000 00:00 0 [heap]
[root@izbp14xswj2tx6qgnz9dllz ~]# python
Python 2.7.5 (default, Aug 4 2017, 00:39:18)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> (0x022ef000-0x022ce000)/1024
132
一 简介
TCMalloc 库全称Thread-Caching Malloc
是谷歌开源工具google-perftools
中的成员,是一个内存分配器,这个内存分配器与我们上面说的linux系统下默认的glibc中的ptmalloc2内存分配器有什么不同那。主要优点:
-
tcmalloc 一次malloc或free操作更快,据说ptmalloc2需要300ns,tcmalloc 需要50ns。
-
tcmalloc 优化小对象的存储,需要更少的空间。
-
tcmalloc 对多线程分配内存,小对象基本不存在锁竞争,因为分配器会给现场分配本地缓存,长期空闲的情况下也不会被回收。
二 分配策略
在TCMalloc 对于不同申请内存的大小采用不同的策略,内存大小划分如下:
-
(0,256KB] 属于小内存;
-
(256KB,1MB] 属于中等内存;
-
(1M, +∞] 属于大内存。在TCMalloc 中内存分配粒度有两种一种是Object,大小是预设的规则,比如8字节,16字节,32字节,48字节等;另一种叫Span,一个Span是由多个Page组成,Object 是由Span切出来的。
2.1 Thread Cache
对于小内存分配,是在Thread Cache中分配的,为了减少内存碎片,TCMalloc按大小划分了85个类别,这些称为Size Class,每个Size Class都对应固定字节的大小,从8个字节,到16个字节…一直到256K,这些 Thread Cache采用双向链表形式串联起来,由于Thread Cache 是每个线程都有的,所以多线程同时分配内存的时候,如果Thread Cache 对应的Size Class中相应大小是空闲,就不用加锁,比如我们申请15B内存,这个线程的Thread Cache中从Size Class中的16字节对应的free list中分配内存。
Thread Cache 在整个分配器中是个双向链表,每个都对应一个空闲内存池。每个线程的Thread Cache大小限制最大4MB,大小在512KB-4MB之间。所有线程Thread Cache的总大小默认限制为32MB,取值范围从512KB到1GB之间。
2.2 Central Cache
Central Cache是所有线程公用的缓存,Central Cache对于每个Size Class 都由一个独立的链表来缓存空闲对象,当Thread Cache中内存不够的时候,从中获取,由于是所有线程公用的,所以从中取或归还对象的时候,需要加锁的,一般Thread Cache一次获取或归还多个空闲对象。
2.3 Page Heap
如果Central Cache的内存不够的话,会向Page Heap申请。申请好的内存,将其拆分成一系列空闲对象,添加对应size class对应的free list中。Page Heap的内存结构是以span为单位组织的,每个span由不同的page 组成,Page Heap按照大小分为1page 对应span,2个pages对应span,一直到128个pages 对应的span,即128*8KB= 1024KB即1M,注意这里面的page为8KB(可以调整默认为8KB)。大小超过1M的,即超过128个pages的span存储在有序的set中,如下图:
2.4 小对象的分配策略
-
将要分配的内存向上取到对应的size class对应的队列中。
-
如果该线程的Thread Cache 对应的size class的free list 非空,取出一个空闲对象 返回。
-
如果该线程对应的size class的free list为空,则向 Central Cache申请内存:
-
-
则向page heap 申请一个span (page heap不够,则向系统申请内存)。
-
将span拆分成若干个size class对象。
-
如果 Central Cache对应的size class 有空闲对象,则取一些空闲对象。
-
如果Central Cache对应的size class 中也无空闲对象:
-
-
将申请到的size class 放到Thread Cache 的空闲队列中,返回一个空闲对象。
2.5 中间对象的分配策略
即大于256KB 小于1MB内存的分配,分配的内存是直接从 page heap上选取与申请内存向上对应的span,比如申请内存3.5个pages,则直接从 4个pages 对应的span查找。假设向上对应的为k个pages:
-
在 k pages 对应的span空闲队列查找,如果没有就向下找 k+1 个pages对应的span。
-
直到找到一个非空的n个pages对应的空闲span或者找到了128个pages对应的span
-
-
从这个找到的n个pages对应的span中切出 k个pages 作为申请内存结果返回。
-
将剩下的n-k个pages 对应的span插入到相应的空闲队列中。
-
-
如果找不到合适的span,则算大对象。
2.6 大对象的内存分配策略
同样是在page heap中分配,由于申请的内存大于128个pages,所以需要在page heap的set里面去找合适的n个pages对应的span。
-
如果找到,拆分:
-
-
一个span大小为k 个page,返回。
-
剩下n- k个pages,如果n-k> 128,仍然放在set中,小于则放在对应的小span中。
-
-
如果找不到,则需要向系统申请内存,申请内存以span为单位。
2.7 内存回收
当应用程序释放内存给Thread Cache,会回收到free list中,如果free list中内存达到一定程度后,会返回给Central Cache中,如果一个span中的所有对象都回收了,则再将span返还给page heap, page heap在合适时机返回给系统。
三 C 程序使用TCMalloc
编译链接下tcmalloc库,就可以了,通过下面的方法验证下:
[root@localhost testcode]# g++ -g -ltcmalloc m.c -o m
[root@localhost testcode]# ./m
^C
[root@localhost testcode]# vim m.c
[root@localhost testcode]# gdb ./m
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-100.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/miaohq/testcode/m...done.
(gdb) b 7
Breakpoint 1 at 0x40071c: file m.c, line 7.
(gdb) r
Starting program: /home/miaohq/testcode/./m
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".Breakpoint 1, main (argc=1, argv=0x7fffffffe118) at m.c:7
7 malloc(1);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-39.el7.x86_64 libstdc++-4.8.5-39.el7.x86_64
(gdb) s
tc_malloc (size=1) at src/tcmalloc.cc:1892
四 诗词欣赏
八声甘州·自王家无怨住襄城[宋] [魏了翁]自王家无怨住襄城,世总生贤。
似谢阶兰玉,马庭梧竹,一一堪怜。
富贵关人何事,且问此何缘。又
踏前朝脚,领蜀山川。
点检重关复阁,尚甘棠匝地,乔木参天。
中兴规画,父老至今传。
六十年、山河未改,只芳菲、不断紧相联。
相将又,参陪宰席,还似当年。