TCMalloc:線程緩衝的Malloc

這段時間比較閒,研究下內存管理,從官方文檔開始啃起《TCMalloc : Thread-Caching Malloc》。html

1.動機

   TCMalloc要比glibc 2.3的malloc(能夠從一個叫做ptmalloc2的獨立庫得到)和其餘我測試過的malloc都快。ptmalloc在一臺2.8GHz的P4機器上執行一次小對象malloc及free大約須要300納秒,而TCMalloc的版本一樣的操做大約只須要50納秒。malloc版本的速度是相當重要的,由於若是malloc不夠快,應用程序的做者就傾向於在malloc之上寫一個本身的內存釋放列表。這就可能致使額外的代碼複雜度,以及更多的內存佔用――除非做者自己很是仔細地劃分釋放列表的大小並常常從中清除空閒的對象。算法

   TCMalloc也減小了多線程程序中的鎖競爭狀況。對於小對象,已經基本上達到了零競爭。對於大對象,TCMalloc嘗試使用恰當粒度和有效的自旋鎖。ptmalloc一樣是經過使用每線程各自的空間來減小鎖的競爭,可是ptmalloc2使用每線程空間有一個很大的問題。在ptmalloc2中,內存不可能會從一個空間移動到另外一個空間。這有可能致使大量內存被浪費。例如,在一個Google的應用中,第一階段可能會爲其URL標準化的數據結構分配大約300MB內存。當第一階段結束後,第二階段將從一樣的地址空間開始。若是第二個階段被安排到了與第一階段不一樣的空間內,這個階段不會複用任何第一階段留下的的內存,並會給地址空間添加另一個300MB。相似的內存爆炸問題也能夠在其餘的應用中看到。數組

   TCMalloc的另外一個好處表如今小對象的空間效率。例如,分配N個8字節對象可能要使用大約8N * 1.01字節的空間,即多用百分之一的空間。而ptmalloc2中每一個對象都使用了一個四字節的頭,我認爲並將最終的尺寸圓整爲8字節的倍數,最後使用了16N字節。緩存

2.使用

   要使用TCMalloc,只要將tcmalloc經過「-ltcmalloc」連接器標誌接入你的應用便可。數據結構

   你也能夠經過使用LD_PRELOAD在不是你本身編譯的應用中使用tcmalloc:多線程

1 $ LD_PRELOAD="/usr/lib/libtcmalloc.so" 

  LD_PRELOAD比較麻煩,咱們也不十分推薦這種用法。app

  TCMalloc還包含了一個堆檢查器以及一個堆測量器less

   若是你更想連接不包含堆測量器和檢查器的TCMalloc版本(好比可能爲了減小靜態二進制文件的大小),你應該連接libtcmalloc_minimalide

3.綜述

   TCMalloc給每一個線程分配了一個線程局部緩存。小對象的分配是直接由線程局部緩存來完成的。若是須要的話會將對象從中央數據結構移動到線程局部緩存中,同時按期的用垃圾收集器把內存從線程局部緩存遷移回中央數據結構中。svn

 

   TCMalloc將尺寸小於等於32K的對象(「小」對象)和大對象區分開來。大對象直接使用頁級分配器(page-level alloctor)(一個頁是一個4K的對齊內存區域)從中央堆直接分配。即,一個大對象老是頁對齊的並佔據了整數個數的頁。

   連續的一些頁面能夠被分割爲一系列相等大小的小對象。例如,一個連續的頁面(4K)能夠被劃分爲32個128字節的對象。

4.小對象分配

   每一個小對象的大小都會被映射到與之接近的 60個可分配的尺寸類別中的一個。例如,全部大小在833到1024字節之間的小對象時,都會歸整到1024字節。60個可分配的尺寸類別這樣隔開:較小的尺寸相差8字節,較大的尺寸相差16字節,再大一點的尺寸差32字節,如此等等。最大的間隔是控制的,這樣剛超過上一個級別被分配到下一個級別就不會有太多的內存被浪費。

   一個線程緩存包含了由各個尺寸內存的對象組成的單鏈表,如圖所示:

image

  當分配一個小對象時:(1)咱們將其大小映射到對應的尺寸中。 (2)查找當前線程的線程緩存中相應的尺寸的內存鏈表。 (3)若是當前尺寸內存鏈表非空,那麼從鏈表中移除的第一個對象並返回它。當咱們按照這種方式分配時,TCMalloc不須要任何鎖。這就能夠極大提升分配的速度,由於鎖/解鎖操做在一個2.8GHz Xeon上大約須要100納秒的時間。

   若是當前尺寸內存鏈表爲空:(1)從Central Heap中取得一系列這種尺寸的對象(Central Heap是被全部線程共享的)。 (2)將他們放入該線程線程的緩衝區。 (3)返回一個新獲取的對象給應用程序。

   若是Central Heap也爲空:(1) 咱們從中央頁分配器分配了一系列頁面。(2) 將他們分割成該尺寸的一系列對象。(3)將新分配的對象放入Central Heap的鏈表上 (4) 像前面同樣,將部分對象移入線程局部的鏈表中。

5.線程緩衝的大小的肯定

   恰當線程緩衝區大小相當重要,若是緩衝區過小,咱們須要常常去Central Heap分配;若是線程緩衝區太大,又導致大量對象閒置而浪費內存。

   注意到恰當的線程緩衝區的大小對內存的釋放同樣重要。若是沒有線程緩衝,每次內存釋放都須要把內存移回到Central Heap。一樣,一些線程有不對稱的內存分配和釋放行爲(例如:生產者和消費者線程),因此肯定恰當的緩衝區大小也很棘手。

   肯定緩衝區大小,咱們採用「慢開始」算法來肯定每個尺寸內存鏈表的最大長度。當某個鏈表使用更頻繁,咱們就擴大他的長度。若是咱們某個鏈表上釋放的操做比分配操做更多,它的最大長度將被增加到整個鏈表能夠一次性有效的移動到Central Heap的長度。

   下面的僞代碼說明了這種慢開始算法。注意到num_objects_to_move對每個尺寸是不一樣的。經過移動特定長度的對象鏈表,中央緩衝能夠高效的將鏈表在線程中傳遞。若是線程緩衝區的須要小於num_objects_to_move,在中央緩衝區上的這種操做具備線性的時間複雜度。使用num_objects_to_move做爲從中央緩衝區傳遞的對象數量的缺點是,它將不須要的那部分對象浪費在線程緩衝區。

 

 1 Start each freelist max_length at 1.
 2 
 3 Allocation
 4 if freelist empty {
 5     fetch min(max_length, num_objects_to_move) from central list;
 6     if max_length < num_objects_to_move {  // slow-start
 7        max_length++;
 8    } else {
 9     max_length += num_objects_to_move;
10   }
11 }
12 
13 Deallocation
14 if length > max_length {
15   // Don't try to release num_objects_to_move if we don't have that many.
16   //release min(max_length, num_objects_to_move) objects to central list
17    if max_length < num_objects_to_move {
18      // Slow-start up to num_objects_to_move.
19       max_length++;
20    } else if max_length > num_objects_to_move {
21      // If we consistently go over max_length, shrink max_length.
22       overages++;
23     if overages > kMaxOverages {
24         max_length -= num_objects_to_move;
25         overages = 0;
26     }
27   }
28 }

 

6.大對象的分配

   一個大對象的尺寸(> 32K)會被中央頁堆處理,被圓整到一個頁面尺寸(4K)。中央頁堆是由 空閒內存列表組成的數組。對於i < 256而言,數組的第k個元素是一個由每一個單元是由k個頁面組成的空閒內存鏈表。第256個條目則是一個包含了長度>= 256個頁面的空閒內存鏈表:

 pageheap

 

  k個頁面的一次分配經過在第k個空閒內存鏈表中查找來完成。若是該空閒內存鏈表爲空,那麼咱們則在下一個空閒內存鏈表中查找,如此繼續。最終,若是必要的話,咱們將在最後空閒內存鏈表中查找。若是這個動做也失敗了,咱們將向系統獲取內存(使用sbrkmmap或者經過在/dev/mem中進行映射)。

  若是k個頁面的分配是由連續的> k個頁面的空閒內存鏈表完成的,剩下的連續頁面將被從新插回到與之頁面大小接近的空閒內存鏈表中去。

7.跨度

   TCMalloc管理的堆由一系列頁面組成。一系列的連續的頁面由一個「跨度」(Span)對象來表示。一個跨度能夠是已被分配或者是空閒的。若是是空閒的,跨度則會是一個頁面堆鏈表中的一個條目。若是已被分配,它會或者是一個已經被傳遞給應用程序的大對象,或者是一個已經被分割成一系列小對象的一個頁面。若是是被分割成小對象的,對象的尺寸類別會被記錄在跨度中。

   由頁面號索引的中央數組能夠用於找到某個頁面所屬的跨度對象。例如,下面的跨度a佔據了2個頁面,跨度b佔據了1個頁面,跨度c佔據了5個頁面最後跨度d佔據了3個頁面,如圖:

 image

  在一個32位的地址空間中,中央數組由一個2層的基數樹來表示,其中根包含了32個條目,每一個葉包含了 215個條目(一個32爲地址空間包含了 220個 4K 頁面(2^32 / 4k),一層則是用25整除220個頁面)。這就致使了中央陣列的初始內存使用須要128KB空間(215*4字節),看上去仍是能夠接受的。

  在64位機器上,咱們將使用一個3層的基數樹。

8.釋放

   當一個對象被釋放時,咱們先計算他的頁面號並在中央數組中查找對應的跨度對象。該跨度會告訴咱們該對象是大是小,若是它是小對象的話尺寸類別是多少。若是是小對象的話,咱們將其插入到當前線程的線程緩存中對應的空閒內存鏈表中。若是線程緩存如今超過了某個預約的大小(默認爲2MB),咱們便運行垃圾收集器將未使用的對象從線程緩存中移入中央自由列表。

   若是該對象是大對象的話,跨度對象會告訴咱們該對象包含的頁面的範圍。假設該範圍是[p,q]。咱們還會查找頁面p-1和頁面q+1對應的跨度對象。若是這兩個相鄰的跨度中有任何一個是空閒的,咱們將他們和[p,q]的跨度接合起來。最後跨度會被插入到頁面堆中合適的空閒鏈表中。

9.小對象的重要空閒內存鏈表

   就像前面提過的同樣,咱們爲每個尺寸類別設置了一箇中央空閒列表。每一箇中央空閒列表由兩層數據結構來組成:一系列跨度和每一個跨度對象的一個空閒內存的鏈表。

   一個對象是經過從某個跨度對象的空閒列表中取出第一個條目來分配的。(若是全部的跨度裏只有空鏈表,那麼首先從中央頁面堆中分配一個尺寸合適的跨度。)

   一個對象經過將其添加到它包含的跨度對象的空閒內存鏈表中來將還回中央空閒列表。若是鏈表長度如今等於跨度對象中全部小對象的數量,那麼該跨度就是徹底自由的了,就會被返回到頁面堆中(跨度對象中全部小對象都回收完了,整個跨度對象就空閒了)。

10.線程緩衝區的垃圾回收

   垃圾回收對象保證線程緩衝區的大小可控制並將未使用的對象交還給中央空閒列表。有的線程須要大量的緩衝來保證工做有很好的性能,而有的線程只須要不多甚至不須要緩衝就能工做,當一個線程的緩衝區超過它的max_size,垃圾回收對象介入,以後這個線程就要和其它線程競爭獲取更大的緩衝。

  垃圾回收僅僅會在內存釋放的時候纔會容許。咱們檢查全部的空閒內存鏈表並把一些數量的對象從空閒列表移動到中央鏈表。

   從某個空閒鏈表中移除的對象的數量是經過使用一個每空閒鏈表的低水位線L來肯定的。L記錄了自上一次垃圾收集以來列表最短的長度。注意,在上一次的垃圾收集中咱們可能只是將列表縮短了L個對象而沒有對中央列表進行任何額外訪問。咱們利用這個過去的歷史做爲對將來訪問的預測器並將L/2個對象從線程緩存空閒列表列表中移到相應的中央空閒鏈表中。這個算法有個很好的特性是,若是某個線程再也不使用某個特定的尺寸時,該尺寸的全部對象都會很快從線程緩存被移到中央空閒鏈表,而後能夠被其餘緩存利用。

  若是在線程中,某個大小的內存對象持續釋放比分配操做多,這種2/L行爲會引發至少有L/2的對象長期處於空閒鏈表中,爲了不這種內存浪費,咱們減小每一個鏈表的最大長度num_objects_to_move個。

 

1 Garbage Collection
2 if (L != 0 && max_length > num_objects_to_move) {
3     max_length = max(max_length - num_objects_to_move, num_objects_to_move)
4 }

  線程的緩衝區超過max_size的事實代表若是提升線程的緩衝區,線程將運行的更加有效率。簡單的提升max_size的值將用掉過分的內存對於一個有不少現場的應用程序。開發者須要經過 –tcmalloc_max_total_thread_cache_bytes標誌來限制內存的用量。

   max_size每一個線程緩衝區從一個最小的max_size(例如64k)開始,這樣空閒的線程就不會在當它們不準的時候預分配內存。每次容許垃圾回收器,若是線程的緩衝區大小小於tcmalloc_max_total_thread_cache_bytes,也會嘗試增長max_size,max_size增加很是容易。不然,線程1將經過減少線程2的max_size來嘗試從線程2偷取內存。採用這種方式,更加活躍的線程將從其它線程偷取內存。這樣大多數空閒線程將保持很小的緩衝區而活躍的線程將保持較大的緩衝區。注意這種偷取可能引發線蟲緩衝區的總和比tcmalloc_max_total_thread_cache_bytes大知道線程2釋放內存到垃圾回收器。

11.性能

11.1 PTMalloc2單元測試

  PTMalloc2包(如今已是glibc的一部分了)包含了一個單元測試程序t-test1.c。它會產生必定數量的線程並在每一個線程中進行一系列分配和解除分配;線程之間沒有任何通訊除了在內存分配器中同步。

   t-test1(放在tests/tcmalloc/中,編譯爲ptmalloc_unittest1)用一系列不一樣的線程數量(1~20)和最大分配尺寸(64B~32KB)運行。這些測試運行在一個2.4GHz 雙核心Xeon的RedHat 9系統上,並啓用了超線程技術, 使用了Linux glibc-2.3.2,每一個測試中進行一百萬次操做。在每一個案例中,一次正常運行,一次使用LD_PRELOAD=libtcmalloc.so

   下面的圖像顯示了TCMalloc對比PTMalloc2在不一樣的衡量指標下的性能。首先,現實每秒操做次數(百萬)以及最大分配尺寸,針對不一樣數量的線程。用來生產這些圖像的原始數據(time工具的輸出)能夠在t-test1.times.txt中找到。

image

 

image

 

  • TCMalloc要比PTMalloc2更具備一致地伸縮性——對於全部線程數量>1的測試,小分配達到了約7~9百萬操做每秒,大分配降到了約2百萬操做每秒。單線程的案例則明顯是要被剔除的,由於他只能保持單個處理器繁忙所以只能得到較少的每秒操做數。PTMalloc2在每秒操做數上有更高的方差——某些地方峯值能夠在小分配上達到4百萬操做每秒,而在大分配上降到了<1百萬操做每秒。
  • TCMalloc在絕大多數狀況下要比PTMalloc2快,而且特別是小分配上。線程間的爭用在TCMalloc中問題不大。
  • TCMalloc的性能隨着分配尺寸的增長而下降。這是由於每線程緩存當它達到了閾值(默認是2MB)的時候會被垃圾收集。對於更大的分配尺寸,在垃圾收集以前只能在緩存中存儲更少的對象。
  • TCMalloc性能在約32K最大分配尺寸附件有一個明顯的降低。這是由於在每線程緩存中的32K對象的最大尺寸;對於大於這個值得對象TCMalloc會從中央頁面堆中進行分配。

  下面是每秒CPU時間的操做數(百萬)以及線程數量的圖像,最大分配尺寸64B~128KB。

image

image

 

  此次咱們再一次看到TCMalloc要比PTMalloc2更連續也更高效。對於<32K的最大分配尺寸,TCMalloc在大線程數的狀況下典型地達到了CPU時間每秒約0.5~1百萬操做,同時PTMalloc一般達到了CPU時間每秒約0.5~1百萬,還有不少狀況下要比這個數字小不少。在32K最大分配尺寸之上,TCMalloc降低到了每CPU時間秒1~1.5百萬操做,同時PTMalloc對於大線程數降到幾乎只有零(也就是,使用PTMalloc,在高度多線程的狀況下,不少CPU時間被浪費在輪流等待鎖定上了)。

12.修改運行行爲

   能夠經過環境變量來控制tcmalloc的行爲,一般有用的標誌。

 

標誌 默認值 做用
TCMALLOC_SAMPLE_PARAMETER 0 採樣時間間隔
TCMALLOC_RELEASE_RATE 1.0 釋放未使用內存的機率
TCMALLOC_LARGE_ALLOC_REPORT_THRESHOLD 1073741824 內存最大分配閾值
TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES 16777216 分配給線程緩衝的最大內存上限

    微調參數:

 

TCMALLOC_SKIP_MMAP default: false If true, do not try to use mmap to obtain memory from the kernel.
TCMALLOC_SKIP_SBRK default: false If true, do not try to use sbrk to obtain memory from the kernel.
TCMALLOC_DEVMEM_START default: 0 Physical memory starting location in MB for /dev/mem allocation. Setting this to 0 disables/dev/mem allocation.
TCMALLOC_DEVMEM_LIMIT default: 0 Physical memory limit location in MB for /dev/mem allocation. Setting this to 0 means no limit.
TCMALLOC_DEVMEM_DEVICE default: /dev/mem Device to use for allocating unmanaged memory.
TCMALLOC_MEMFS_MALLOC_PATH default: "" If set, specify a path where hugetlbfs or tmpfs is mounted. This may allow for speedier allocations.
TCMALLOC_MEMFS_LIMIT_MB default: 0 Limit total memfs allocation size to specified number of MB. 0 means "no limit".
TCMALLOC_MEMFS_ABORT_ON_FAIL default: false If true, abort() whenever memfs_malloc fails to satisfy an allocation.
TCMALLOC_MEMFS_IGNORE_MMAP_FAIL default: false If true, ignore failures from mmap.
TCMALLOC_MEMFS_MAP_PRVIATE default: false If true, use MAP_PRIVATE when mapping via memfs, not MAP_SHARED.

13.在代碼中修改行爲

   在malloc_extension.h中的MallocExtension類提供了一些微調的接口來修改tcmalloc的行爲來使得你的程序達到更高的效率。

13.1 歸還內存給操做系統

  默認狀況下,tcmalloc將逐漸的釋放長時間未使用的內存給內核。tcmalloc_release_rate標誌控制歸還給操做系統內存的速度大,你也能夠長治釋放內存經過執行以下操做:

1  MallocExtension::instance()->ReleaseFreeMemory();

  你一樣能夠調用SetMemoryReleaseRate()來在運行時修改tcmalloc_release_rate的值,或者調用GetMemoryReleaseRate來查看當前釋放的機率值。

13.2 內存診斷

  有幾種操做能夠獲取可讀的當前內存的使用狀況: 

1 MallocExtension::instance()->GetStats(buffer, buffer_length);
2 MallocExtension::instance()->GetHeapSample(&string);
3 MallocExtension::instance()->GetHeapGrowthStacks(&string);

  後面兩個方法建立如同heap-profiler同樣的文件格式,能夠直接傳遞給pprof。第一個方法主要用於調試。

13.3 通常的Tcmalloc狀態

   tcmalloc支持設置和獲取狀態屬性 

1 MallocExtension::instance()->SetNumericProperty(property_name, value);
2 MallocExtension::instance()->GetNumericProperty(property_name, &value);

  設置這些屬性對於應用程序而言是惋惜的,最經常使用的是當庫設置了屬性,這樣應用程序就能夠都這些屬性。這裏是Tcmalloc定義的屬性,你能夠獲取它們經過調用接口,好比MallocExtension::instance()->GetNumericProperty("generic.heap_size", &value);

generic.current_allocated_bytes Number of bytes used by the application. This will not typically match the memory use reported by the OS, because it does not include TCMalloc overhead or memory fragmentation.
generic.heap_size Bytes of system memory reserved by TCMalloc.
tcmalloc.pageheap_free_bytes Number of bytes in free, mapped pages in page heap. These bytes can be used to fulfill allocation requests. They always count towards virtual memory usage, and unless the underlying memory is swapped out by the OS, they also count towards physical memory usage.
tcmalloc.pageheap_unmapped_bytes Number of bytes in free, unmapped pages in page heap. These are bytes that have been released back to the OS, possibly by one of the MallocExtension "Release" calls. They can be used to fulfill allocation requests, but typically incur a page fault. They always count towards virtual memory usage, and depending on the OS, typically do not count towards physical memory usage.
tcmalloc.slack_bytes Sum of pageheap_free_bytes and pageheap_unmapped_bytes. Provided for backwards compatibility only. Do not use.

14. 附加說明

   對於某些系統,TCMalloc可能沒法與沒有連接libpthread.so(或者你的系統上同等的東西)的應用程序正常工做。它應該能正常工做於使用glibc 2.3的Linux上,可是其餘OS/libc的組合方式還沒有通過任何測試。

   TCMalloc可能要比其餘malloc版本在某種程度上更吃內存,(可是傾向於不會有其餘malloc版本中可能出現的爆發性增加。)尤爲是在啓動時TCMalloc會分配大約240KB的內部內存。

   不要試圖將TCMalloc載入到一個運行中的二進制程序中(例如,在Java中使用JNI)。二進制程序已經使用系統malloc分配了一些對象,並會嘗試將它們傳遞到TCMalloc進行解除分配。TCMalloc是沒法處理這種對象的。

 

原文連接:http://blog.csdn.net/chen19870707/article/details/40039401

相關文章
相關標籤/搜索