1. 程式人生 > >[Paper翻譯]Scalable Lock-Free Dynamic Memory Allocation

[Paper翻譯]Scalable Lock-Free Dynamic Memory Allocation

free mas point 出現 前綴 影響 ati bit 同步

原文: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.87.3870&rep=rep1&type=pdf

Abstract

動態內存分配器(malloc/free)在多線程環境下依靠互斥鎖來保護共享數據的一致性。使用鎖在性能,可用性,健壯性,程序靈活性方面有很多缺點。Lock-free的內存分配器能消除線程延遲或被殺死以及CPU的調度策略對程序的性能影響。這篇paper呈上了一個完整的無鎖內存分配器。它的實現只使用被廣泛支持的操作系統API和硬件原子指令。即使出現線程中斷(thead termination)或死機故障(crash-failure),它也是可用的。由於是無鎖的,它避免了死鎖的情況。另外,我們的分配器是高度可擴展的,我們把空間溢出限制為常數因子,同時能夠避免false sharing.另外,我們的分配器有優越的並發性能和極低的延遲。

1. Introduction

動態內存分配函數在多線程應用中廣泛使用,應用範圍從商業數據庫,web服務器到數據挖掘,科學性質應用。目前的分配器使用互斥鎖來保證線程安全。

Lock-free同步(synchronization)是一個合適的實現線程安全的替換方案。它有以下優點:

免疫死鎖

異步信號安全(asynv-signal-safety):如果使用了互斥鎖,他們將不會使用信號,因為無法保證異步信號安全。假設一個線程持有用戶級的鎖,這時它接受到一個信號,信號處理程序調用分配器,該程序也會請求同樣的互斥鎖,但是被中斷的線程正持有這個鎖。信號處理程序等待被中斷的線程釋放互斥鎖,但是,在信號處理程序完成前,線程不會恢復。這就造成了死鎖。因此,在使用互斥鎖後,這些實現智能屏蔽中斷或者在malloc,free時使用內核級別的鎖。但是這樣會損失很多性能。相反,一個無鎖分配器能夠保證異步信號安全,而不損失任何性能。

容忍優先倒置(tolerance to priority inversion):用戶級別的鎖很容易因為優先倒置導致死鎖。高優先的線程A等待一個低優先的線程B釋放鎖,但是直到高優先的A完成前,低優先的線程B不會被調度。無鎖同步能夠無視線程的調度策略。

Kill-tolerant availability:一個無鎖對象能夠免疫死鎖,即使在任意多的線程在操作它時被kill。這對於高可用的服務器很有用,這能使程序容忍不頻繁的進程損失,來緩解臨時的資源短缺。

搶占容忍(preemption-tolerance):當一個線程持有互斥鎖時,其他線程搶占了處理器,但是由於搶占線程也在等待同樣的鎖,因此搶占線程不能執行下去。從持有鎖的線程被搶占,到處理器重新調度持有鎖的線程完成任務並施放鎖,這之間的時間相當於是被浪費的時間。而無鎖同步不在意線程的調度情況。

每個線程從自己的堆上請求內存,並釋放塊(blocks)到自己的堆上。然而,這是難以接受的解決方案,因為這導致了無限的內存消耗。即使這個程序的內存需求實際上很小。其他不可接受的特點,包括需要初始化很大一片地址空間,人為設置總體大小。限制特定線程或特定內存塊大小的地址的預請求區域。可接受的方案應該是廣泛適用且節省空間的,不應該強加對地址空間的使用限制。

為了建設我們的無鎖分配器只使用簡單的當前主流處理器支持的原子指令,我們把malloc和free分解為幾個原子操作,組織分配器的數據結構

2. 原子指令

我們只需處理器支持Compare-and-swap(CAS)或者load-linked和store-conditionnal(LL/SC)的組合兩者之一即可。像fetch-and-add或者swap都可以由CAS或LL/SC實現。

3. 實現

這個實現是在64位地址空間的。32位的版本會更加簡單。同時64位的CAS操作兼容32位架構。

首先,我們來介紹這個分配器的結構。大型blocks將直接從OS請求,並釋放到OS。對於小型blocks。較大的superblocks(比如16KB)組成heap.每一個superblock分割為多個大小相等的block。superblock根據block的size分布在size classes中。每個size class包含多個processor heap(即procheap結構),heap的數量與處理器的數量成正比。每個processor heap最多有一個active狀態的superblock。active狀態的superblock包含一個及以上數量的可用block。當用戶請求一個可用block,superblock將會把一個可用block設為保留狀態,以此保證線程能通過processor heap的地址訪問到block。每一個superblock對應一個descriptor結構。每一個被請求的block包含8字節的指向superblock的前綴。在第一次調用malloc時,size classes結構和procheap結構(about 16 KB for a processor machine)將被請求並初始化。

調用mallock時,procheap一般已經有一個活躍狀態的superblock了。線程原子地讀取指向active superblock(descriptor結構)的指針,並保留一個block。然後,線程原子地從superblock中pop一個block,並更新descriptor結構。調用free時,push一個被釋放的block到原先superblock的可用block的列表中,並更新descriptor結構。

之前我們講述了三個主要結構:size class結構(保存),procheap結構,和descriptor結構。這裏,還有兩個輔助結構,anchor結構和active結構。anchor是decriptor的輔助結構。Active是procheap的輔助結構。如果proheap結構中的active域不為NULL,那就說明當前有active狀態的superblock可用。當調用malloc,線程讀取active結構,原子地對credits減1,然後驗證active superblock是否仍然為active。

// Superblock descriptor structure
typedef anchor : // fits in one atomic block
unsigned avail:10,  //index of the first available block in the superblock
count:10,  //count holds the number of unreserved blocks in the superblock
state:2,    //state holds the state of the superblock
tag:42;    //is used to prevent the ABA problem
// state codes ACTIVE=0 FULL=1 PARTIAL=2 EMPTY=3

typedef descriptor :
anchor Anchor;
descriptor* Next;
void* sb; // pointer to superblock
procheap* heap; // pointer to owner procheap
unsigned sz; // block size
unsigned maxcount; // superblock size/sz

// Processor heap structure
typedef active : 
unsigned ptr:58, //a pointer to the descriptor of the active superblock owned by   the processor heap
credits:6; // number of blocks available for reservation in the active superblock less one

typedef procheap :
active Active; // initially NULL
descriptor* Partial; // initially NULL
sizeclass* sc; // pointer to parent sizeclass

// Size class structure
typedef sizeclass :
descList Partial; // initially empty
unsigned sz; // block size
unsigned sbsize; // superblock size

在descriptor結構中的Anchor域包含有原子更新的子域。子域avail持有這個super block中第一個可用內存塊的index。子域count持有superblock中已使用的內存塊的個數。子域state持有superblock的狀態。子域tag用來避免ABA問題。

處理器堆結構中的active域是指向該處理器擁有的當前活躍的superblock的descriptor的指針。如果active的值不為NULL,就保證了活躍superblock有至少一個內存塊可用。子域credits持有活躍superblock可用內存塊數減1,如果credits的值為n,那麽superblock包含n+1個可用內存塊。在一個典型的malloc操作中(比如當active!=NULL and credits>0),線程在驗證活躍superblock仍然有效後,讀取active然後原子地對credits減1。

superblock有以下四種狀態:active,full,partial,empty。ACTIVE表示這個superblock在這個堆中是活躍superblock,或者一個線程正試圖將它安裝為active。FULL表示這個superblock中所有block都已經被分配或被保留。PARTIAL表示這個superblock是非ACTIVE的,並且包含未保留的可用的block。EMPTY表示如果free這個superblock是安全操作。

malloc算法.

如果block的size很大,block將直接從OS請求內存,它的前綴被設置為與size相關。不然,相應的堆用被請求的block大小和線程id標記。然後,線程試圖以下述順序請求block。

  1. 從堆的活躍superblock請求一個block.
  2. 如果沒有活躍的superblock,試圖從PARTIAL的superblock請求一個block.
  3. 如果都沒有,那麽請求一個新的superblock並嘗試設置了ACTIVE
void* malloc(sz) {
// Use sz and thread id to find heap.
 heap = find heap(sz);
 if (!heap) // Large block
 Allocate block from OS and return its address.
while(1) {
 addr = MallocFromActive(heap);
 if (addr) return addr;
 addr = MallocFromPartial(heap);
 if (addr) return addr;
 addr = MallocFromNewSB(heap);
 if (addr) return addr;
} }

Malloc form active superblock算法

絕大多數malloc請求經由此算法返回。此算法主要分為兩步。第一步(代碼1-6)請求讀取指向active superblock的指針然後原子地對active結構中的credits域減1。對credits減1這個動作保留出1個block,然後檢查active superblock是否仍然有效。在CAS成功之後,線程保證了1個block被保留且可用。第二步(代碼7-18)對LIFO(即棧)進行lock-free的pop操作。線程從anchor.avail域中讀取第一個可用block的index,然後讀取下一個可用block的index。最後驗證之前讀取的第二個index確實為現在棧中的第二個index,然後把指針指向第二個可用block。

僅當anchor.avail等於oldanchor.avail時才驗證為有效。線程X從anchor.avail從讀取了值A,從*addr中讀取了值B。讀取B後,其他線程搶占了CPU並pop了block A,又pop了block B,最後push了block A回來。之後,線程A恢復了,並執行CAS。如果沒有anchor.tag域,CAS將會發現anchor等於oldanchor並錯誤地執行swap操作,實際上,第二個可用block已經不是block B了。為了防止這個ABA問題,我們使用了IBM經典的tag機制。每當pop時,我們都對anchor.tag加1。這樣,在上述情況發生時,由於anchor.tag不等於oldanchor.tag,CAS操作將會正確地返回false,程序返回循環頂部。tag的位數(bits)必須足夠大,因為它只能遞增。使用LL/SC原子操作能在原理層面避免ABA問題。

13-17行,線程檢查這次操作是否提取了最後一個credit。如果確實是最後一個(credit==0),那麽檢查superblock是否有更多可用的block(由於descriptor.maxcount大於MAXCREDITS或者有block剛被free)。如果有更多block可用(count>0),線程將盡可能多地保留block。否則(count=0),將這個superblock聲明為FULL

當這個線程提取了credit,它會試著執行UpdateActive函數來更新Active結構。多線程同時提取向一個superblock提取credit並沒有ABA問題。最後,線程存儲descriptor結構的地址到新請求的block 的最前部,以便於當block之後被free時能確定這個block來自於哪個superblock。每個block包含8字節的前部。

在行6的CAS成功後,行18的CAS成功前,superblock的狀態可能由ACTIVE變為FULL或PARTIAL,或者變為另一個procheap的active superblock(但是必須是同一個size class)。但是這些都對原線程沒有影響。

void* MallocFromActive(heap) {
do { // First step: reserve block
 newactive = oldactive = heap->Active;
 if (!oldactive) return NULL;
 if (oldactive.credits == 0)
 newactive = NULL;
else
 newactive.credits--;
 } until CAS(&heap->Active,oldactive,newactive);
// Second step: pop block
 desc = mask credits(oldactive);
do {
// state may be ACTIVE, PARTIAL or FULL
 newanchor = oldanchor = desc->Anchor;
 addr = desc->sb+oldanchor.avail*desc->sz;
 next = *(unsigned*)addr;
 newanchor.avail = next;
 newanchor.tag++;
 if (oldactive.credits == 0) {
// state must be ACTIVE
 if (oldanchor.count == 0)
 newanchor.state = FULL;
else {
 morecredits = min(oldanchor.count,MAXCREDITS);
 newanchor.count -= morecredits;
}
}
 } until CAS(&desc->Anchor,oldanchor,newanchor);
 if (oldactive.credits==0 && oldanchor.count>0)
 UpdateActive(heap,desc,morecredits);
 *addr = desc; return addr+EIGHTBYTES;
}

UpdateActive

當heap上沒有active superblock時,調用UpdateActive會將desc->sb重新註冊為當前active的superblock。然而,在重新註冊之前,其他線程有可能註冊了一個新的superblock。如果發生後述的情況,當前線程必須返回credit,並把superblock設為PARTIAL,再放入procheap.partial中,以供將來使用。

UpdateActive(heap,desc,morecredits) {
 newactive = desc;
 newactive.credits = morecredits-1;
 if CAS(&heap->Active,NULL,newactive) return;
// Someone installed another active sb
// Return credits to sb and make it partial
do {
 newanchor = oldanchor = desc->Anchor;
 newanchor.count += morecredits;
 newanchor.state = PARTIAL;
 } until CAS(&desc->Anchor,oldanchor,newanchor);
 HeapPutPartial(desc);
}

MallocFromPartial

當線程發現procheap.active==NULL時,它會調用mallocFromPartial。線程試圖調用HeapGetPartial來獲取PARTIAL狀態的superblock。如果成功獲取了一個superblock,它會盡可能多地保留block,在行10的CAS成功之後,線程成功保留了多個block。然後從它保留的block中pop一個出來供該線程使用,如果還有多余的block,就將這個superblock設為active,否則設為FULL。

在HeapGetPartial中,線程首先試圖從procheap.partial從提取superblock,如果提取不到,那麽從對應的size class的partial list中提取。

void* MallocFromPartial(heap) {
retry:
 desc = HeapGetPartial(heap);
 if (!desc) return NULL;
 desc->heap = heap;
do { // reserve blocks
 newanchor = oldanchor = desc->Anchor;
 if (oldanchor.state == EMPTY) {
 DescRetire(desc); goto retry;
}
// oldanchor state must be PARTIAL
// oldanchor count must be > 0
 morecredits = min(oldanchor.count-1,MAXCREDITS);
 newanchor.count -= morecredits+1;
 newanchor.state = (morecredits > 0) ? ACTIVE : FULL;
 } until CAS(&desc->Anchor,oldanchor,newanchor);
 { // pop reserved block
 newanchor = oldanchor = desc->Anchor;
 addr = desc->sb+oldanchor.avail*desc->sz;
 newanchor.avail = *(unsigned*)addr;
 newanchor.tag++;
 } until CAS(&desc->Anchor,oldanchor,newanchor);
 if (morecredits > 0)
 UpdateActive(heap,desc,morecredits);
 *addr = desc; return addr+EIGHTBYTES;
}

descriptor* HeapGetPartial(heap) {
do {
 desc = heap->Partial;
 if (desc == NULL)
 return ListGetPartial(heap->sc);
 } until CAS(&heap->Partial,desc,NULL);
 return desc;
}

Malloc from new superblock

如果線程找不到PARTIAL狀態的superblock,它將調用mallocFromNewSB。線程調用DescAlloc請求一個新的descriptor,然後初始化這個descriptor(行2-11)。最後,用CAS嘗試在procHeap.active註冊這個superblock。當註冊失敗,說明有新的active superblock已經註冊,那麽就刪除該線程生成的這個desciptor和對應的superblock,去使用那個已註冊的active superblock。當然你也可以使用自己註冊的這個superblock,並設置為PARTIAL。我們為了避免太多的PARTIAL superblock和因此產生的不必要的碎片,我們更傾向於直接free這個superblock。

在內存一致性(memory consistency)弱於順序一致性(sequential consistency)的系統中,處理器可能無序地執行和觀察內存訪問,內存屏障可以用來確保內存訪問的順序。行12的內存屏障確保在該superblock註冊成功前,相應的descriptor結構同步到其他處理器。如果沒有這個內存屏障,在CAS之後,其他處理器上的線程可能讀到過時的值。

void* MallocFromNewSB(heap) {
 desc = DescAlloc();
 desc->sb = AllocNewSB(heap->sc->sbsize);
 Organize blocks in a linked list starting with index 0.
 desc->heap = heap;
 desc->Anchor.avail = 1;
 desc->sz = heap->sc->sz;
 desc->maxcount = heap->sc->sbsize/desc->sz;
 newactive = desc;
 newactive.credits =
min(desc->maxcount-1,MAXCREDITS)-1;
 desc->Anchor.count =
(desc->maxcount-1)-(newactive.credits+1);
 desc->Anchor.state = ACTIVE;
 memory fence.
 if CAS((&heap->Active,NULL,newactive) {
 addr = desc->sb;
 *addr = desc; return addr+EIGHTBYTES;
} else {
 Free the superblock desc->sb.
 DescRetire(desc); return NULL;
}
}

後面還有些free算法和性能檢測, 就不翻譯了

[Paper翻譯]Scalable Lock-Free Dynamic Memory Allocation