天天看點

TCMalloc:線程緩存的Malloc [Webkit有其應用]

作者:Sanjay Ghemawat, Paul Menage

<a href="http://goog-perftools.sourceforge.net/doc/tcmalloc.html" target="_blank">原文</a>

TCMalloc也減少了多線程程式中的鎖争用情況。對于小對象,幾乎已經達到了零争用。對于大對象,TCMalloc嘗試使用粒度較好和有效的自旋鎖。ptmalloc同樣是通過使用每線程各自的場地來減少鎖争用,但是ptmalloc2使用每線程場地有一個很大的問題。在ptmalloc2中,記憶體可能會從一個場地移動到另一個。這有可能導緻大量空間被浪費。例如,在一個Google的應用中,第一階段可能會為其URL标準化的資料結構配置設定大約300MB記憶體。當第一階段結束後,第二階段将從同樣的位址空間開始。如果第二個階段被安排到了一個與第一階段什?用的場地不同的場地,這個階段不會複用任何第一階段留下的的記憶體,并會給位址空間添加另外一個300MB。類似的記憶體爆炸問題也可以在其他的應用中看到。

TCMalloc的另一個好處是小對象的空間最優表現形式。例如,配置設定N個8位元組對象可能要使用大約<code>8N * 1.01</code>位元組的空間。即,多用百分之一的空間。而ptmalloc2中每個對象都使用了一個四位元組的頭,(我認為)并将最終的尺寸規整為8位元組的倍數,最後使用了<code>16N</code>位元組。

要使用TCMalloc,隻要将tcmalloc通過“-ltcmalloc”連結器标志接入你的應用即可。

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

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

如果你更想連結不包含堆測量器和檢查器的TCMalloc版本(比如可能為了減少靜态二進制檔案的大小),你可以接入<code>libtcmalloc_minimal</code>。

TCMalloc給每個線程配置設定了一個線程局部緩存。小配置設定可以直接由線程局部緩存來滿足。需要的話,會将對象從中央資料結構移動到線程局部緩存中,同時定期的垃圾收集将用于把記憶體從線程局部緩存遷移回中央資料結構中。

TCMalloc:線程緩存的Malloc [Webkit有其應用]

TCMalloc将尺寸小于&lt;=

32K的對象(“小”對象)和大對象區分開來。大對象直接使用頁級配置設定器(一個頁是一個4K的對齊記憶體區域)從中央堆直接配置設定。即,一個大對象總是頁對齊的并占據了整數個數的頁。

連續的一些頁面可以被分割為一系列小對象,而他們的大小都相同。例如,一個連續的頁面(4K)可以被劃分為32個128位元組的對象。

每個小對象的大小都會被映射到170個可配置設定的尺寸類别中的一個。例如,在配置設定961到1024位元組時,都會歸整為1024位元組。尺寸類别這樣隔開:較小的尺寸相差8位元組,較大的尺寸相差16位元組,再大一點的尺寸差32位元組,如此類推。最大的間隔(對于尺寸 &gt;= ~2K的)是256位元組。

一個線程緩存對每個尺寸類都包含了一個自由對象的單向連結清單。

TCMalloc:線程緩存的Malloc [Webkit有其應用]

當配置設定一個小對象時:

我們将其大小映射到對應的尺寸類中。

查找目前線程的線程緩存中相應的自由清單。

如果自由清單不空,那麼從移除清單的第一個對象并傳回它。當按照這個快速通道時,TCMalloc不會擷取任何鎖。這就可以極大提高配置設定的速度,因為鎖/解鎖操作在一個2.8GHz Xeon上大約需要100納秒的時間。

如果自由清單為空:

從該尺寸類别的中央自由清單(中央自由清單是被所有線程共享的)取得一連串對象。

将他們放入線程局部的自由清單。

将新擷取的對象中的一個傳回給應用程式。

如果中央自由清單也為空:(1) 我們從中央頁配置設定器配置設定了一連串頁面。(2) 将他們分割成該尺寸類的一系列對象。(4) 像前面一樣,将部分對象移入線程局部的自由清單中。

一個大對象的尺寸(&gt; 32K)會被除以一個頁面尺寸(4K)并取整(大于結果的最小整數),同時是由中央頁面堆來處理的。中央頁面堆又是一個自由清單的陣列。對于<code>i &lt; 256</code>而言,第<code>k</code>個條目是一個由<code>k</code>個頁面組成的自由清單。第<code>256</code>個條目則是一個包含了長度<code>&gt;= 256</code>個頁面的自由清單:

TCMalloc:線程緩存的Malloc [Webkit有其應用]

<code>k</code>個頁面的一次配置設定通過在第<code>k</code>個自由清單中查找來完成。如果該自由清單為空,那麼我們則在下一個自由清單中查找,如此繼續。最終,如果必要的話,我們将在最後一個自由清單中查找。如果這個動作也失敗了,我們将向系統擷取記憶體(使用<code>sbrk</code>、<code>mmap</code>或者通過在<code>/dev/mem</code>中進行映射)。

如果<code>k</code>個頁面的一次配置設定行為由連續的長度&gt; <code>k</code>的頁面滿足了,剩下的連續頁面将被重新插回到頁面堆的對應的自由清單中。

TCMalloc管理的堆由一系列頁面組成。連續的頁面由一個“跨度”(<code>Span</code>)對象來表示。一個跨度可以是已被配置設定或者是自由的。如果是自由的,跨度則會是一個頁面堆連結清單中的一個條目。如果已被配置設定,它會是一個已經被傳遞給應用程式的大對象,或者是一個已經被分割成一系列小對象的一個頁面。如果是被分割成小對象的,對象的尺寸類别會被記錄在跨度中。

由頁面号索引的中央數組可以用于找到某個頁面所屬的跨度。例如,下面的跨度a占據了2個頁面,跨度b占據了1個頁面,跨度c占據了5個頁面最後跨度d占據了3個頁面。

TCMalloc:線程緩存的Malloc [Webkit有其應用]

在一個32位的位址空間中,中央陣列由一個2層的基數樹來表示,其中根包含了32個條目,每個葉包含了 215個條目(一個32為位址空間包含了 220個 4K 頁面,是以這裡樹的第一層則是用25整除220個頁面)。這就導緻了中央陣列的初始記憶體使用需要128KB空間(215*4位元組),看上去還是可以接受的。

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

當一個對象被解除配置設定時,我們先計算他的頁面号并在中央陣列中查找對應的跨度對象。該跨度會告訴我們該對象是大是小,如果它是小對象的話尺寸類别是什麼。如果是小對象的話,我們将其插入到目前線程的線程緩存中對應的自由清單中。如果線程緩存現在超過了某個預定的大小(預設為2MB),我們便運作垃圾收集器将未使用的對象從線程緩存中移入中央自由清單。

如果該對象是大對象的話,跨度會告訴我們該對象覆寫的頁面的範圍。假設該範圍是<code>[p,q]</code>。我們還會查找頁面<code>p-1</code>和頁面<code>q+1</code>對應的跨度。如果這兩個相鄰的跨度中有任何一個是自由的,我們将他們和<code>[p,q]</code>的跨度接合起來。最後跨度會被插入到頁面堆中合适的自由清單中。

就像前面提過的一樣,我們為每一個尺寸類别設定了一個中央自由清單。每個中央自由清單由兩層資料結構來組成:一系列跨度和每個跨度一個自由對象的連結清單。

通過從某個跨度中移除第一個條目來從中央自由清單配置設定一個對象。(如果所有的跨度裡隻有空連結清單,那麼首先從中央頁面堆中配置設定一個尺寸合适的跨度。)

一個對象可以通過将其添加到他包含的跨度的連結清單中來傳回到中央自由清單中。如果連結清單長度現在等于跨度中所有小對象的數量,那麼該跨度就是完全自由的了,就會被傳回到頁面堆中。

某個線程緩存當緩存中所有對象的總共大小超過2MB的時候,會對他進行垃圾收集。垃圾收集門檻值會自動根據線程數量的增加而減少,這樣就不會因為程式有大量線程而過度浪費記憶體。

我們會周遊緩存中所有的自由清單并且将一定數量的對象從自由清單移到對于得中央清單中。

從某個自由清單中移除的對象的數量是通過使用一個每清單的低水位線<code>L</code>來确定的。<code>L</code>記錄了自上一次垃圾收集以來清單最短的長度。注意,在上一次的垃圾收集中我們可能隻是将清單縮短了<code>L</code>個對象而沒有對中央清單進行任何額外通路。我們利用這個過去的曆史作為對未來通路的預測器并将<code>L/2</code>個對象從線程緩存自由清單中移到相應的中央自由清單中。這個算法有個很好的特性是,如果某個線程不再使用某個特定的尺寸時,該尺寸的所有對象都會很快從線程緩存被移到中央自由清單,然後可以被其他緩存利用。

PTMalloc2包(現在已經是glibc的一部分了)包含了一個單元測試程式<code>t-test1.c</code>。它會産生一定數量的線程并在每個線程中進行一系列配置設定和解除配置設定;線程之間沒有任何通信除了在記憶體配置設定器中同步。

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

下面的圖像顯示了TCMalloc對比PTMalloc2在不同的衡量名額下的性能。首先,現實每秒全部操作(百萬)以及最大配置設定尺寸,針對不同數量的線程。用來生産這些圖像的原始資料(<code>time</code>工具的輸出)可以在<code>t-test1.times.txt</code>中找到。

TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]

TCMalloc要比PTMalloc2更具有一緻地伸縮性——對于所有線程數量&gt;1的測試,小配置設定達到了約7~9百萬操作每秒,大配置設定降到了約2百萬操作每秒。單線程的案例則明顯是要被剔除的,因為他隻能保持單個處理器繁忙是以隻能獲得較少的每秒操作數。PTMalloc2在每秒操作數上有更高的方差——某些地方峰值可以在小配置設定上達到4百萬操作每秒,而在大配置設定上降到了&lt;1百萬操作每秒。

TCMalloc在絕大多數情況下要比PTMalloc2快,并且特别是小配置設定上。線程間的争用在TCMalloc中問題不大。

TCMalloc的性能随着配置設定尺寸的增加而降低。這是因為每線程緩存當它達到了門檻值(預設是2MB)的時候會被垃圾收集。對于更大的配置設定尺寸,在垃圾收集之前隻能在緩存中存儲更少的對象。

TCMalloc性能在約32K最大配置設定尺寸附件有一個明顯的下降。這是因為在每線程緩存中的32K對象的最大尺寸;對于大于這個值得對象TCMalloc會從中央頁面堆中進行配置設定。

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

TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]
TCMalloc:線程緩存的Malloc [Webkit有其應用]

這次我們再一次看到TCMalloc要比PTMalloc2更連續也更高效。對于&lt;32K的最大配置設定尺寸,TCMalloc在大線程數的情況下典型地達到了CPU時間每秒約0.5~1百萬操作,同時PTMalloc通常達到了CPU時間每秒約0.5~1百萬,還有很多情況下要比這個數字小很多。在32K最大配置設定尺寸之上,TCMalloc下降到了每CPU時間秒1~1.5百萬操作,同時PTMalloc對于大線程數降到幾乎隻有零(也就是,使用PTMalloc,在高度多線程的情況下,很多CPU時間被浪費在輪流等待鎖定上了)。

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

TCMalloc可能要比其他malloc版本在某種程度上更吃記憶體,(但是傾向于不會有其他malloc版本中可能出現的爆發性增長。)尤其是在啟動時TCMalloc會配置設定大約240KB的内部記憶體。

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