TCMalloc原理



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

 

一、動機

 

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字節。

 

二、使用

 

要使用TCMalloc,只要將tcmalloc通過“-ltcmalloc”鏈接器標誌接入你的應用即可。

 

你也可以通過使用LD_PRELOAD在不是你自己編譯的應用中使用tcmalloc:

 

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

 

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

 

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

 

如果你更想鏈接不包含堆測量器和檢查器的TCMalloc版本(比如可能爲了減少靜態二進制文件的大小),你應該鏈接 libtcmalloc_minimal

 

三、綜述

 

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

 

 

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

 

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

 

四、小對象分配

 

每個小對象的大小都會被映射到與之接近的 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) 像前面一樣,將部分對象移入線程局部的鏈表中。

 

五、線程緩衝的大小的確定

 

恰當線程緩衝區大小至關重要,如果緩衝區太小,我們需要經常去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:   }

 

六、大對象的分配

 

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

 

pageheap

 

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

 

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

 

七、跨度

 

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層的基數樹。

八、釋放

 

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

 

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

 

九、小對象的重要空閒內存鏈表

 

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

 

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

 

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

 

十、線程緩衝區的垃圾回收

 

垃圾回收對象保證線程緩衝區的大小可控制並將未使用的對象交還給中央空閒列表。有的線程需要大量的緩衝來保證工作有很好的性能,而有的線程只需要很少甚至不需要緩衝就能工作,當一個線程的緩衝區超過它的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釋放內存到垃圾回收器。

 

十一、性能

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時間被浪費在輪流等待鎖定上了)。

 

十二、修改運行行爲

 

可以通過環境變量來控制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.

 

十三、在代碼中修改行爲

 

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

 

歸還內存給操作系統

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

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

你同樣可以調用SetMemoryReleaseRate()來在運行時修改tcmalloc_release_rate的值,或者調用GetMemoryReleaseRate來查看當前釋放的概率值。

 

內存診斷

有幾種操作可以獲取可讀的當前內存的使用情況:

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

 

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

 

一般的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.

 

十四、附加說明

 

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

 

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

 

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

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