C++应用程序性能优化(八)——内存分配机制

2021-03-10 05:27

阅读:776

标签:图片   munmap   堆栈   加锁   log   内存地址   函数   才有   length   

C++应用程序性能优化(八)——内存分配机制

一、操作系统内存布局

1、32位系统经典内存布局

Linux Kernel 2.6.7前版本采用的默认内存布局形式如下:
技术图片
(1)32操作系统中,loader将可执行文件的各个段次依次载入到从0x80048000(128M)位置开始的空间中。应用程序能够访问的最后地址是0xbfffffff(3G)的位置,3G以上的位置是给内核使用的,应用程序不能直接访问。
(2)内存布局从低地址到高地址依次为:txet段、data段、bss段、heap、mmap映射区、stack栈区。
(3)heap和mmap是相对增长的,heap只有1G的虚拟地址空间可供使用。?
(4)stack空间不需要映射,用户可以直接访问栈空间,因此是利用堆栈溢出进行***的基础。
起始1GB地址为内核空间,随后是向下增加的栈空间和由0x40000000向上增加的MMAP地址;堆空间从底部开始,去除ELF、数据段、代码段、常量段后的地址,并向上增长。缺点是容易遭受溢出***,堆地址空间只有不到1GB。

2、32位系统默认内存布局

Linux Kernel 2.6.7版本后32位操作系统的默认内存布局方式如下:
技术图片
在经典内存布局基础上增加了Random offset随机偏移,不容易遭受溢出***;堆地址向上增长,但MMAP向下增长,栈空间不是动态增长的,会受到限制;内存地址利用率较高。
栈自顶向下扩展,但栈有边界,因此栈大小有限制(ulimit ?-s查看)。堆自底向上扩展,mmap映射区自顶向下扩展,mmap和heap是相对扩展,直至消耗尽虚拟地址空间中的剩余区域。

3、64位系统内存布局

技术图片
64位操作系统的寻址空间比较大,沿用32位操作系统的经典内存布局,增加随机MMAP地址,防止溢出***。

二、操作系统内存管理机制

1、操作系统内存管理简介

内存管理自底向上分为三个层次:
(1)操作系统内核的内存管理。
(2)glibc层使用系统调用维护的内存管理算法。
(3)应用程序从glibc动态分配内存后,根据应用程序本身的程序特性进行优化,比如使用引用计数std::shared_ptr,内存池方式等等。
应用程序可以直接使用系统调用从内核分配内存,根据程序特性自己维护内存,但会大大增加开发成本。

2、操作系统内存管理机制

Linux Kernel内存管理的基本思想是内存延迟分配,即只有在真正访问一个地址的时候才建立地址的物理映射。Linux Kernel在用户申请内存的时候,只分配一个虚拟地址,并没有分配实际物理地址,只有当用户使用内存时,Linux Kernel才会分配具体的物理地址给用户使用。
对于大内存,通常不同的内存分配方式都是直接MMAP;对于小数据,则通过向操作系统申请扩大堆顶,操作系统会把内存分页映射到进程堆空间,再由malloc管理内存堆块,减少系统调用;free内存时,不同内存分配方式有不同策略,不一定会将内存还给操作系统,因此如果访问释放的内存并不会立即Run Time Error,只有访问的地址没有对应的内存分页才会。
对于heap操作,操作系统提供brk系统调用,c运行库提供sbrk库函数;对于mmap映射区操作,操作系统提供了mmap和munmap系统调用。

3、heap操作系统调用接口

#include 
int brk(void *addr);
void *sbrk(intptr_t increment);

当参数increment为0时,sbrk返回进程当前的brk值,increment为正数时扩展brk值,increment为负数时收缩brk值。
brk是系统调用,sbrk是库函数。C语言的动态内存分配基本函数是malloc,在glibc中malloc函数调用库函数sbrk,sbrk调用brk函数。brk系统调用只是简单的改变mm_struct结构体的成员变量brkd的值。
技术图片
start_code和end_code是进程代码段的起始和结束地址、
start_data和end_data是进程数据段的起始和终止地址。
start_stack是进程堆栈段的起始地址。
start_brk是进程动态内存分配的起始地址(堆的起始地址)。
brk是进程动态内存分配当前的终止地址(堆的当前最后地址)。

4、mmap操作系统调用接口

#include 
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t length);

addr:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志。
flags:指定映射对象的类型,映射选项和映射页是否可以共享。
fd:有效的文件描述符。
offset:被映射对象内容的起点。?
mmap函数将一个文件或者其它对象映射进内存,文件被映射到多个页上,如果文件大小不是所有页大小之和,最后一个页不被使用的空间将会清零;munmap函数删除特定地址区域的对象映射。

三、ptmalloc2简介

1、ptmalloc2简介

ptmalloc/ptmalloc2/ptmalloc3版本基于dlmalloc进行开发,
Wolfram Gloger在Doug Lea的基础上对dlmalloc进行了改进,使得ptmalloc可以支持多线程。Linux操作系统默认使用glibc malloc版本,即ptmalloc2,glibc-2.3.x中已经集成了ptmalloc2,目前ptmalloc最新版本为ptmalloc3。
ptmalloc内存分配器处于内核和用户程序之间,用于响应用户的内存分配请求,向操作系统申请内存,然后将内存返回给用户程序。为了保证高效,内存分配器一般都会预先分配一块大于用户请求的内并进行管理;用户释放掉的内存不会立即返回给操作系统,内存分配器会管理被释放掉的空闲空间以应对用户以后的内存分配请求。内存分配器不仅需要管理分配的内存块,还需要管理空间的内存块。当响应用户的请求时,内存分配器会首先在空闲空间中寻找一块合适的内存返回给用户,在空闲空间中找不到时才会重新分配一块新内存。

2、ptmalloc2多线程支持

dlmalloc内存分配器中只有一个主分配区,每次分配内存都必须对主分配区加锁,分配完成后再释放锁。在多线程的环境下,对主分配区的锁的竞争很激烈,严重影响内存分配效率。
ptmalloc内存分配器可以支持多线程,增加了非主分配区支持,主分配区和非主分配区形成一个环形链表进行管理,每一个分配区使用互斥锁保证多线程访问安全。每个进程只有一个主分配区,可以有多个非主分配区,ptmalloc根据系统对分配区的争用情况动态的增加非主分配区的个数,分配区的数量一旦增加就不会再减少。
主分配区可以访问进程的heap和mmap映射区域,即主分配区可以使用brk、sbrk、mmap向操作系统申请虚拟内存;非主分配区只能访问进程的mmap映射区域,非主分配区每次使用mmap系统调用向操作系统申请HEAP_MAX_SIZE(32位系统1M,64位系统64M)大小的虚拟内存,当用户向非主分配区请求分配内存时再分割成小块内存进行分配。
由于系统调用的效率比较低,直接从用户空间分配内存效率会比较高,因此ptmalloc只在必要情况下才会调用mmap系统调用向操作系统申请内存。
主分配区可以访问heap区域,如果用户不调用sbrk或者brk函数,分配程序就可以保证分配到连续的虚拟内存,因为一个进程只有一个主分配区使用sbrk分配heap区域的虚拟内存。如果主分配区的内存是通过mmap向系统申请的,当free内存时,主分配区会直接调用munmap将内存归还给操作系统。
当线程需要使用malloc函数分配内存空间时,线程会先查看线程的私有变量中是否已经存在有一个分配区。如果存在,尝试对分配区加锁,如果加锁成功就使用相应分配区分配内存;如果失败,线程就搜索环形链表试图获得一个没有加锁的分配区来分配内存;如果所有分配区都加锁,那么malloc会开辟一个新分配区,把新分配区添加到循环链表中并加锁,使用新分配区进行分配内存操作。
在释放操作中,线程试图获得待释放内存块所在的分配区的锁,如果分配区正在被其它线程使用,则需要等待其它线程释放分配区的互斥锁后才可以进行释放操作。?
为了支持多线程,多个线程可以从同一个分配区中分配内存,ptmalloc假设线程A释放掉一块内存后,线程B会申请类似大小的内存,但A释放的内存跟B需要的内存不一定完全相等,可能有一个小的误差,就需要不停地对内存块作切割和合并,因此分配过程中可能产生内存碎片。

3、ptmalloc2缺点

(1)ptmalloc收缩内存从top chunk开始,如果与top chunk相邻的chunk不能释放,top chunk以下都不能释放。
(2)多线程锁开销大,需要避免多线程频繁分配释放。
(3)多线程使用内存不均衡时,容易导致内存浪费。
(4)每个chunk至少8字节的开销很大。
(5)不定期分配长生命周期的内存容易造成内存碎片,不利于回收。

四、ptmalloc2 BINS架构

1、chunk简介

若每次申请内存都调用sbrk、brk、mmap,那么每次都会产生系统调用,影响性能,并且容易产生内存碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。
ptmalloc使用内存池管理方式,采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了高效分配内存,ptmalloc会预先向操作系统申请一块内存供用户使用,当申请和释放内存时,ptmalloc会对相应内存块进行管理,并通过内存分配策略判断是否将其回收给操作系统,使用户申请和释放内存更加高效,避免产生过多的内存碎片。
用户请求分配的内存在ptmalloc中使用chunk表示, 每个chunk至少需要8个字节额外的开销。 用户释放掉的内存不会马上归还操作系统,ptmalloc会统一管理heap和mmap区域的空闲chunk,避免了频繁的系统调用。

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;    /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;         /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;           /* double links -- used only if free. */
  struct malloc_chunk* bk;
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize;  /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

prev_size: 如果前一个chunk空闲,表示前一个chunk的大小;如果前一个chunk不空闲,字段无意义。?
size:当前chunk的大小,并且记录了当前chunk和前一个chunk的一些属性,包括前一个chunk是否在使用中,当前chunk是否是通过mmap获得的内存,当前chunk是否属于非主分配区。?
fd和bk:指针fd和bk只有当chunk空闲时才存在,用于将对应的空闲chunk块加入到空闲chunk块链表中统一管理,如果chunk 块被分配给应用程序使用,chunk块被从空闲chunk链表中拆出,那么这两个指针也就没有用了,所以也当作应用程序的使用空间,而不至于浪费。?
fd_nextsize和bk_nextsize:当前chunk存在于large bins 中时, large bins中空闲chunk按照大小排序,但同一个大小的 chunk可能有多个,增加fd_nextsize和bk_nextsize字段可以加快遍历空闲chunk ,并查找满足需要的空闲chunk, fd_nextsize 指向下一个比当前chunk大小大的第一个空闲chunk, bk_nextsize指向前一个比当前chunk大小小的第一个空闲 chunk 。
使用中的chunk结构如下:
技术图片
chunk指针指向chunk开始的地址;
mem指针指向用户内存块开始的地址。
P=0时,表示前一个chunk为空闲,prev_size才有效;P=1时,表示前一个chunk正在使用,prev_size无效;P主要用于内存块的合并操作。ptmalloc分配的第一个块总是将P设为1,以防止程序引用到不存在的区域。?
M=1为mmap映射区域分配;M=0为heap区域分配。
A=1为非主分区分配;A=0为主分区分配。
空闲chunk结构如下:
技术图片
空闲的chunk会被放置到空闲的链表bins上。当用户申请分配内存时,会先去查找空闲链表bins上是否有合适的内存。
fp和bp分别指向前一个和后一个空闲的chunk。
fp_nextsize和bp_nextsize分别指向前一个空闲chunk和后一个空闲chunk的大小,用于在空闲链表上快速查找合适大小的chunk。

2、BINS架构简介

ptmalloc将相同大小的chunk用双向链表链接起来,称为bin。ptmalloc共维护128个bin,并使用一个数组来存储bin。
技术图片
数组中第1个bin为unsorted bin,如果被用户释放的chunk大于max_fast或者fast bins中的空闲chunk合并后,chunk首先会被放到unsorted bin队列中。如果unsorted bin不能满足分配要求,ptmalloc会将unsorted bin中的chunk加入bins中,然后再从bins中继续进行查找和分配过程。unsorted bin是bins的一个缓冲区,可以加快内存分配速度。
数组中第2个至第64个的bin称为small bin,同一个small bin 中的chunk具有相同的大小,相邻的small bin中的chunk大小相差为8字节。
数组中第65个开始bin称为large bin。large bins中的每一个 bin分别包含了一个给定范围内的chunk,其中chunk按大小序排列,相同大小的chunk按照最近使用顺序排列。

3、Fast Bins

程序在运行时会经常需要申请和释放小块内存空间。当内存分配器合并相邻的若干chunk后,可能立即会有另一个小块内存的请求,内存分配器需要从大块空闲内存中分割出一块内存,效率低下,因此ptmalloc在分配过程中引入了fast bins。
fast bins是bins的高速缓冲区,每个fast bin是空闲chunk的单链表,采用单链表是由于fast bin中增删chunk都发生在链表的前端。fast bins中包含7个fast bin,fast bin中的chunk大小以8字节递增,分别为16、24、32、40、48、56、64。?
当用户申请分配不大于max_fast(默认值64字节)内存块时,ptmalloc首先会先到fast bins中相应chunk尺寸的fast bin寻找是否有合适chunk,在fast bin中被检索出的第一个chunk将被摘除并返回给用户;当用户释放一块不大于max_fast(默认值64字节)的chunk时,默认会被放到fast bins中chunk尺寸大小的fast bin的前端,除非特定情况,两个相邻空闲chunk并不会被合并成一个空闲chunk,不合并可能会导致碎片化问题,但可以大大加速释放过程。

4、Unsorted Bin

Unsorted Bin存储在bins数组的第1个,是bins的缓冲区,用于加快内存分配速度。当用户释放的chunk大于max_fast或者fast bins中合并后的chunk都会首先进入unsorted bin上。Unsorted Bin用双向链表管理chunk,chunk无大小限制,任何大小chunk都可以添加进Unsorted Bin。Unsorted Bin提供了ptmalloc重复使用最近释放的chunk,因此内存的分配和释放会更快。?
用户申请分配内存时,如果在fast bins中没有找到合适的chunk,则ptmalloc会先在Unsorted Bin中查找合适的空闲chunk;如果没有合适的chunk,ptmalloc会将unsorted bin上的chunk放入bins上,然后继续到bins上查找合适的空闲chunk。?

5、Small Bins

小于512字节的chunk即small chunk,保存small chunk的bin即small bin。bins数组从第2至第64的63个bin为Small Bin,Small Bins中相邻bin之间相差8个字节,同一个Small Bin中的chunk具有相同大小,第0个Small Bin中的chunk大小为16字节,第62个Small Bin中的chunk大小为512字节。
每个Small Bin是一个空闲chunk的双向循环链表,释放掉的chunk添加在链表的前端,而分配出的chunk则从链表后端摘除并返回给用户。?
分配chunk时,从相应chunk大小的small bin中摘除最后一个chunk并返回给用户;释放chunk时,ptmalloc会检查相邻chunk是否空闲,若是则将相邻chunk从所属的small bin中摘除并合并成一个新chunk,新chunk会添加在unsorted bin的前端;合并chunk消除了碎片化的影响但减慢了释放速度。?

6、Large Bins

大小大于等于512字节的chunk即Large Chunk,保存Large Chunk的bin即Large Bin,位于bins数组中small bins后。Large Bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小递减排序,大小相同则按照最近使用时间排列。?
Large Bins包含63个bin,每个bin中的chunk大小不是一个固定的等差数列,而是分成6组;每组bin中chunk数量是一个固定的等差数列,依次为32、16、8、4、2、1,chunk大小公差依次为64B、512B、4096B、32768B、262144B等。

7、Top Chunk?

top chunk是分配区的顶部空闲内存,当bins上都不能满足内存分配要求时,就会在top chunk上进行分配。?
当top chunk大小比用户所请求大小还大的时候,top chunk会分割为两个部分:User chunk(用户请求大小)和Remainder chunk(剩余大小),Remainder chunk会成为新的top chunk;当top chunk大小小于用户所请求chunk大小时,top chunk会通过sbrk(主分配区)或mmap(非主分配区)系统调用来扩容。??
主分配区是唯一能够映射进程heap区域的分配区,可以通过sbrk来增大和收缩进程heap的大小。ptmalloc在开始的时候会预先分配一块较大的空闲内存。主分配区的top chunk在第一次调用malloc时会分配一块空间作为初始化的heap。
非主分配区会预先从mmap区域分配一块较大的空闲内存模拟 sub-heap,通过管理sub-heap来响应用户的需求,因为内存是按地址从低向高进行分配的,在空闲内存的最高处存在着一块空闲 chunk,即top chunk。当fast bin和bins都满足不了用户的内存分配需求时,ptmalloc会从top chunk分出一块内存给用户,如果top chunk空间不足,会重新分配一个sub-heap,将top chunk迁移到新的sub-heap上。在分配过程中,top chunk的大小随着切割动态变化。
主分配区是唯一能够映射进程heap区域的分配区,可以通过sbrk来增大或收缩进程heap大小。top chunk在heap的最上面,如果申请内存时,top chunk空间不足,ptmalloc会调用sbrk将进程heap的边界brk上移,然后修改top chunk的大小。

8、mmaped chunk?

当需要分配的chunk足够大,fast bins和bins都不能满足要求,甚至top chunk都不能满足分配需求时,ptmalloc会使用mmap来直接使用页映射机制来将页映射到进程空间,放到mmaped chunk上,当释放mmaped chunk上内存的时候会直接交还给操作系统。
mmap分配阈值默认为128KB,分配阈值可以动态调整。如果开启mmap分配阈值的动态调整机制,并且当前回收的chunk大小大于mmap分配阈值,将mmap分配阈值设置为chunk的大小,将mmap收缩阈值设定为mmap分配阈值的2倍。

9、Last remainder chunk?

Last remainder chunk是一种特殊chunk,不会在任何bins中找到。当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需要的small chunk大小,last remainder chunk被分割成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chunk。

C++应用程序性能优化(八)——内存分配机制

标签:图片   munmap   堆栈   加锁   log   内存地址   函数   才有   length   

原文地址:https://blog.51cto.com/9291927/2567655


评论


亲,登录后才可以留言!