C++應用程序性能優化(八)——內存分配機制

C++應用程序性能優化(八)——內存分配機制

一、操作系統內存佈局

1、32位系統經典內存佈局

Linux Kernel 2.6.7前版本採用的默認內存佈局形式如下:
C++應用程序性能優化(八)——內存分配機制
(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位操作系統的默認內存佈局方式如下:
C++應用程序性能優化(八)——內存分配機制
在經典內存佈局基礎上增加了Random offset隨機偏移,不容易遭受溢出***;堆地址向上增長,但MMAP向下增長,棧空間不是動態增長的,會受到限制;內存地址利用率較高。
棧自頂向下擴展,但棧有邊界,因此棧大小有限制(ulimit  -s查看)。堆自底向上擴展,mmap映射區自頂向下擴展,mmap和heap是相對擴展,直至消耗盡虛擬地址空間中的剩餘區域。


3、64位系統內存佈局

C++應用程序性能優化(八)——內存分配機制
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 <unistd.h>
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的值。
C++應用程序性能優化(八)——內存分配機制
start_code和end_code是進程代碼段的起始和結束地址、
start_data和end_data是進程數據段的起始和終止地址。
start_stack是進程堆棧段的起始地址。
start_brk是進程動態內存分配的起始地址(堆的起始地址)。
brk是進程動態內存分配當前的終止地址(堆的當前最後地址)。






4、mmap操作系統調用接口

#include <sys/mman.h>
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結構如下:
C++應用程序性能優化(八)——內存分配機制
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結構如下:
C++應用程序性能優化(八)——內存分配機制
空閒的chunk會被放置到空閒的鏈表bins上。當用戶申請分配內存時,會先去查找空閒鏈表bins上是否有合適的內存。
fp和bp分別指向前一個和後一個空閒的chunk。
fp_nextsize和bp_nextsize分別指向前一個空閒chunk和後一個空閒chunk的大小,用於在空閒鏈表上快速查找合適大小的chunk。














2、BINS架構簡介

ptmalloc將相同大小的chunk用雙向鏈表鏈接起來,稱爲bin。ptmalloc共維護128個bin,並使用一個數組來存儲bin。
C++應用程序性能優化(八)——內存分配機制
數組中第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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章