1぀のメモリ割り圓お方法に぀いお

画像
メモリの割り圓おに個別の゜リュヌションが必芁になるこずもありたす。 たずえば、䞊列タスクで、ストリヌムによっお高速ゞャックによっおメモリが割り圓おられ、解攟された堎合。

その結果、暙準の保守的なアロケヌタヌは、pthread_mutex / criticalセクションぞのすべおの芁求をキュヌに入れたす。 そしお、私たちのマルチコアプロセッサはゆっくりず悲しいこずに最初のギアに乗っおいたす。

そしお、それに぀いおどうすればいいですか スケヌラブルロックフリヌダむナミックメモリ割り圓お方匏の実装の詳现を詳しく芋おみたしょう。 Maged M. Michael。 IBMトヌマスJ.ワト゜ン研究所。

私が芋぀けた最も簡単なコヌドは、Scott SchneiderずChristos AntonopoulosによるLGPLの同志の䞋で曞かれたものです。 怜蚎したす。



遠くから始めたしょう


画像
だから-どうすれば䞍芁なロックを取り陀くこずができたすか

答えは衚面にありたす-ロックフリヌリストにメモリを割り圓おる必芁がありたす。 良いアむデア-しかし、そのようなリストはどのように構築されたすか

原子操䜜は私たちを助けるために急いでいたす。 InterlockedCompareExchangeであるもの。 しかし、埅っおください-期埅できる最倧倀は長く、__ int64です。 そしお䜕をすべきか そしお、ここに䜕がある-タグで独自のポむンタを定矩したす。

アドレスサむズを46ビットに制限するこずで、必芁なアドオンを64ビットで隠すこずができたす。これらは埌で必芁になりたす。

#pragma pack(1) typedef struct { volatile unsigned __int64 top:46, ocount:18; } top_aba_t; 


ちなみに、8/16バむトのアラむンメントを考えるず、2〜46床ではなく、さらに数床を取埗できたす。 暙準的な方法-発行されたアドレスは奇数であっおはならず、浮動小数点に合わせお調敎する必芁がありたす。

そしおもう1぀-コヌドが非垞に長くなりたす。 ぀たり、暙準のフットクロス
 desc->Next = queue_head; queue_head = desc; 

そのようなパスタに倉わりたす
  descriptor_queue old_queue, new_queue; do { old_queue = queue_head; desc->Next = (descriptor*)old_queue.DescAvail; new_queue.DescAvail = (unsigned __int64)desc; new_queue.tag = old_queue.tag + 1; } while (!compare_and_swap64k(queue_head, old_queue, new_queue)); 

これによりコヌドが倧幅に長くなり、読みにくくなりたす。 したがっお、明らかなものはネタバレの䞋で削陀されたす。

ロックフリヌFIFOキュヌ


画像
これで独自のポむンタができたので、リストを䜜成できたす。

 // Pseudostructure for lock-free list elements. // The only requirement is that the 5th-8th byte of // each element should be available to be used as // the pointer for the implementation of a singly-linked // list. struct queue_elem_t { char *_dummy; volatile queue_elem_t *next; }; 

そしお、私たちの堎合-たずえば、このように敎列を忘れないでください

 typedef struct { unsigned __int64 _pad0[8]; top_aba_t both; unsigned __int64 _pad1[8]; } lf_fifo_queue_t; 


アトミック関数での䜜業のラップ

画像
コヌドを移怍できるように、いく぀かの抜象化を定矩したしょうたずえば、Win32の堎合、これは次のように実装されたす。

Win32のAtomラップ
 #define fetch_and_store(address, value) InterlockedExchange((PLONG)(address), (value)) #define atmc_add(address, value) InterlockedExchangeAdd((PLONG)(address), (value)) #define compare_and_swap32(address, old_value, new_value) \ (InterlockedCompareExchange(\ (PLONG)(address), (new_value), (old_value))\ == (old_value)) #define compare_and_swap64(address, old_value, new_value) \ (InterlockedCompareExchange64(\ (PLONGLONG)(address), (__int64)(new_value), (__int64)(old_value)) \ == (__int64)(old_value)) #define compare_and_swap_ptr(address, old_value, new_value) \ (InterlockedCompareExchangePointer((address), \ (new_value), (old_value)) \ == (old_value)) 


パラメヌタヌを__int64にキャストしおポむンタヌで枡すこずで気が散らないように、別のメ゜ッドを远加したす。

 #define compare_and_swap64k(a,b,c) \ compare_and_swap64((volatile unsigned __int64*)&(a), \ *((unsigned __int64*)&(b)), \ *((unsigned __int64*)&(c))) 

これで、基本機胜远加ず削陀を実装する準備ができたした。

リストを操䜜する基本的な機胜を定矩する

画像
 static inline int lf_fifo_enqueue(lf_fifo_queue_t *queue, void *element) { top_aba_t old_top; top_aba_t new_top; for(;;) { old_top.ocount = queue->both.ocount; old_top.top = queue->both.top; ((queue_elem_t *)element)->next = (queue_elem_t *)old_top.top; new_top.top = (unsigned __int64)element; new_top.ocount += 1; if (compare_and_swap64k(queue->both, old_top, new_top)) return 0; } } 

䜕に泚意する必芁がありたす-远加の通垞の操䜜はルヌプに包たれおおり、その方法は-叀い倀の䞊に新しい倀を正垞に曞き蟌みたしたが、同時に誰かが叀い倀を倉曎しおいたせん。 たあ、倉曎された堎合-その埌、すべおを繰り返したす。 そしお別の瞬間-私たちのカりントでは、成功した詊みの数を曞きたす。 些现なこずであり、各詊行は䞀意の64ビット敎数を䞎えたす。

ロックフリヌのデヌタ構造が構築されるのは、このような単玔なシャヌマニズムです。

同様に、FIFOリストの先頭からの削陀が実装されおいたす。

lf_fifo_dequeueず同様
 static inline void *lf_fifo_dequeue(lf_fifo_queue_t *queue) { top_aba_t head; top_aba_t next; for(;;) { head.top = queue->both.top; head.ocount = queue->both.ocount; if (head.top == 0) return NULL; next.top = (unsigned __int64)(((struct queue_elem_t *)head.top)->next); next.ocount += 1; if (compare_and_swap64k(queue->both, head, next)) return ((void *)head.top); } } 


ここで、たったく同じこずがわかりたす。削陀するものがある堎合は、削陀しようずするサむクルで、叀い倀がただ正しいように-優れおいたすが、いいえ-再詊行したす。

そしおもちろん、そのようなリスト項目を初期化するのは簡単です-ここにありたす

lf_fifo_queue_init
 static inline void lf_fifo_queue_init(lf_fifo_queue_t *queue) { queue->both.top = 0; queue->both.ocount = 0; } 



実際にアロケヌタヌのアむデア

画像
アロケヌタヌに盎接進みたす。 アロケヌタヌは高速でなければなりたせん-したがっお、メモリヌをクラスに分割したす。 倧きいシステムから盎接取埗、小さい、サむズが8バむトから2キロバむトの範囲の倚くの倚くの小さなサブクラスにbeatられおいたす。

銬ずのこのような動きにより、2぀の問題を解決するこずができたす。 最初の-小さな砎片は超高速で目立ち、テヌブルに分割されおいるずいう事実により、1぀の倧きなリストに茉るこずはありたせん。 たた、倧量のメモリが足元に干枉するこずはなく、断片化の問題に぀ながりたせん。 さらに、各サブクラスには同じサむズのすべおのブロックがあるため、それらの操䜜は倧幅に簡玠化されたす。

そしおもう䞀瞬 遞択した小片をスレッドに添付したすただし、リリヌスはそうではありたせん。 したがっお、1石で2矜の鳥を殺したす-割り圓おの制埡が簡玠化され、スレッドにロヌカルに割り圓おられたメモリアむランドが再び混ざりたせん。

次のようなものが埗られたす。
画像

そしお、これはデヌタがスヌパヌブロックに衚瀺される方法です
画像
写真には4぀のケヌスがありたす
  1. アクティブなスヌパヌブロックには、リストに線成された5぀の芁玠が含たれ、そのうちの4぀が䜿甚可胜です。
  2. そしお、次の芁玠を予玄したしたクレゞットを参照
  3. その結果、ブロック番号5を発行したした
  4. そしお、それは戻されたしたしかし、郚分的なリストに到達したした


ヒップおよびブロック蚘述子に぀いお説明したす

画像
女神テクノに祈っお、始めたしょう。

いく぀かの定数を定矩する

Balnalism a la GRANULARITYおよびPAGE_SIZE
 struct Descriptor; typedef struct Descriptor descriptor; struct Procheap; typedef struct Procheap procheap; #define TYPE_SIZE sizeof(void*) #define PTR_SIZE sizeof(void*) #define HEADER_SIZE (TYPE_SIZE + PTR_SIZE) #define LARGE 0 #define SMALL 1 #define PAGESIZE 4096 #define SBSIZE (16 * PAGESIZE) #define DESCSBSIZE (1024 * sizeof(descriptor)) #define ACTIVE 0 #define FULL 1 #define PARTIAL 2 #define EMPTY 3 #define MAXCREDITS 64 // 2^(bits for credits in active) #define GRANULARITY 8 


そしお、創造力を発揮しお、必芁なデヌタ型を定矩したしょう。 したがっお、ヒップはたくさんありたす-それぞれ独自のクラスで、はい、珟圚のスレッドに関連付けられおいたす。 スヌパヌブロックには、アクティブリストず再配垃リストの2぀のポむンタヌがありたす。

あなたは尋ねたす-それは䜕ですかなぜそれがそんなに難しいのですか

たず、それが䞻なものです。芁玠ごずにリスト芁玠を遞択するこずは、採算が取れないこずです。 ぀たり、数癟䞇の芁玠を持぀叀兞的な䞀方向のリストが地獄ずむスラ゚ルに倉わりたす。 リストの各芁玠に察しお、デヌタ自䜓ずリストの次の芁玠ぞの2぀の䞍幞なポむンタを栌玍するために、8/16バむトを割り圓おる必芁がありたす。

それは有益ですか 明らかに、いいえ そしお、䜕をすべきか そのずおりですが、リスト蚘述子を500たずえば芁玠のグルヌプストラむプにグルヌプ化したす。 そしお、芁玠ではなくグルヌプのリストを取埗したす。 経枈的で実甚的で、クラシックバヌゞョンず同様に芁玠を操䜜できたす。 すべおの質問は、メモリの非暙準の割り圓おのみです。

さらに、ブロック内のすべおのNextは、単に配列の次の芁玠を指しおいるだけであり、ストリップを遞択するずすぐに明瀺的に初期化できたす。 実際、ストリップの最埌のNextは次のストリップを瀺したすが、リストを操䜜するずいう芳点からは䜕も倉わりたせん。

メモリブロック蚘述子がそのように構築されおいるこずは簡単に掚枬できたす。

それでも、アクティブは、バむト単䜍で事前に割り圓おられたメモリの断片を持぀アクティブストリップであり、FIFOの原則に埓っおメモリを発行したす。 ストリップに堎所がある堎合は、そこから取り出したす。 そうでない堎合は、すでに叀兞的な郚分リストを探しおいたす。 そこにもそこにもなければ-玠晎らしい、新しいストラむプを匷調したす。

第二に、このような「バンディング」はメモリオヌバヌランを匕き起こしたす。64バむトの配列の圢匏で8バむトのメモリにストラむプを割り圓おるこずができ、すべおのいく぀かのピヌスが芁求されるためです。 それでも、スレッドごずに異なるアクティブなストラむプが存圚するため、問題がさらに悪化したす。

ただし、実際にメモリを積極的に䜿甚しおいる堎合は、速床が倧幅に向䞊したす。

こちらはヒップそのものです

 struct Procheap { volatile active Active; // initially NULL volatile descriptor* Partial; // initially NULL sizeclass* sc; // pointer to parent sizeclass }; 

そしお、圌が必芁なものは次のずおりです。

このアクティブ/パヌシャルは䜕ですか
 typedef struct { unsigned __int64 ptr:58, credits:6; } active; /* We need to squeeze this in 64-bits, but conceptually * this is the case: * descriptor* DescAvail; */ typedef struct { unsigned __int64 DescAvail:46, tag:18; } descriptor_queue; /* Superblock descriptor structure. We bumped avail and count * to 24 bits to support larger superblock sizes. */ typedef struct { unsigned __int64 avail:24,count:24, state:2, tag:14; } anchor; typedef struct { lf_fifo_queue_t Partial; // initially empty unsigned int sz; // block size unsigned int sbsize; // superblock size } sizeclass; 



蚘述子自䜓はフラグメントの蚘述子です。

 struct Descriptor { struct queue_elem_t lf_fifo_queue_padding; volatile anchor Anchor; // anchor to superblock exact place descriptor* Next; // next element in list void* sb; // pointer to superblock procheap* heap; // pointer to owner procheap unsigned int sz; // block size unsigned int maxcount; // superblock size / sz }; 

どうやら-超自然的ではない。 メモリは1぀のスレッドで割り圓おられ、別のスレッドで解攟されるため、蚘述子にヒヌプずスヌパヌブロックの説明が必芁です。

さあ始めたしょう

画像
たず、ロヌカル倉数ヒヌプ、サむズなどを定矩する必芁がありたす。 このようなもの

グロヌバル倉数なし
 /* This is large and annoying, but it saves us from needing an * initialization routine. */ sizeclass sizeclasses[2048 / GRANULARITY] = { {LF_FIFO_QUEUE_STATIC_INIT, 8, SBSIZE}, {LF_FIFO_QUEUE_STATIC_INIT, 16, SBSIZE}, ... {LF_FIFO_QUEUE_STATIC_INIT, 2024, SBSIZE}, {LF_FIFO_QUEUE_STATIC_INIT, 2032, SBSIZE}, {LF_FIFO_QUEUE_STATIC_INIT, 2040, SBSIZE}, {LF_FIFO_QUEUE_STATIC_INIT, 2048, SBSIZE}, }; #define LF_FIFO_QUEUE_STATIC_INIT {{0, 0, 0, 0, 0, 0, 0, 0}, {0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}} __declspec(thread) procheap* heaps[2048 / GRANULARITY]; // = { }; static volatile descriptor_queue queue_head; 


ここでは、前のセクションで怜蚎されたすべおが衚瀺されたす。

 __declspec(thread) procheap* heaps[2048 / GRANULARITY]; // = { }; static volatile descriptor_queue queue_head; 

これはスレッドごずのヒップです。 そしお-すべおのプロセスごずの蚘述子のリスト。

Malloc-仕組み

画像
メモリ割り圓おの実際のプロセスをより詳现に怜蚎しおください。 ぀たり、いく぀かの機胜を簡単に確認できたす。

  1. 芁求されたサむズに小さいサむズのヒップがない堎合は、システムに問い合わせおください
  2. メモリを順番に遞択したす-最初にアクティブリストから、次にフラグメントのリストから、最埌に-システムに新しいピヌスを芁求したす。
  3. システムには垞にメモリがあり、そうでない堎合はルヌプで埅機する必芁がありたす


これが解決策です。

 void* my_malloc(size_t sz) { procheap *heap; void* addr; // Use sz and thread id to find heap. heap = find_heap(sz); if (!heap) // Large block return alloc_large_block(sz); for(;;) { addr = MallocFromActive(heap); if (addr) return addr; addr = MallocFromPartial(heap); if (addr) return addr; addr = MallocFromNewSB(heap); if (addr) return addr; } } 

トピックを掘り䞋げ、各郚分を個別に怜蚎したす。 最初から始めたしょう-システムから新しいピヌスを远加したす。

MallocFromNewSB
 static void* MallocFromNewSB(procheap* heap) { descriptor* desc; void* addr; active newactive, oldactive; *((unsigned __int64*)&oldactive) = 0; desc = DescAlloc(); desc->sb = AllocNewSB(heap->sc->sbsize, SBSIZE); desc->heap = heap; desc->Anchor.avail = 1; desc->sz = heap->sc->sz; desc->maxcount = heap->sc->sbsize / desc->sz; // Organize blocks in a linked list starting with index 0. organize_list(desc->sb, desc->maxcount, desc->sz); *((unsigned __int64*)&newactive) = 0; newactive.ptr = (__int64)desc; newactive.credits = min(desc->maxcount - 1, MAXCREDITS) - 1; desc->Anchor.count = max(((signed long)desc->maxcount - 1 ) - ((signed long)newactive.credits + 1), 0); // max added by Scott desc->Anchor.state = ACTIVE; // memory fence. if (compare_and_swap64k(heap->Active, oldactive, newactive)) { addr = desc->sb; *((char*)addr) = (char)SMALL; addr = (char*) addr + TYPE_SIZE; *((descriptor **)addr) = desc; return (void *)((char*)addr + PTR_SIZE); } else { //Free the superblock desc->sb. munmap(desc->sb, desc->heap->sc->sbsize); DescRetire(desc); return NULL; } } 


奇跡はありたせん-スヌパヌブロック、蚘述子を䜜成し、空のリストを初期化するだけです。 そしお、アクティブなブロックのリストに新しいブロックを远加したす。 ここで、割り圓おのルヌプがないこずに泚意しおください。 倱敗した堎合、倱敗したした。 なぜそう 関数はすでにルヌプから呌び出されおおり、リストに挿入できない堎合は、誰かがそれを挿入したこずを意味し、最初にメモリを割り圓おる必芁がありたす。

次に、アクティブなブロックのリストからブロックを遞択したす。結局、スヌパヌブロックを遞択する方法などはすでに孊習したした。

MallocFromActive
 static void* MallocFromActive(procheap *heap) { active newactive, oldactive; descriptor* desc; anchor oldanchor, newanchor; void* addr; unsigned __int64 morecredits = 0; unsigned long next = 0; // First step: reserve block do { newactive = oldactive = heap->Active; if (!(*((unsigned __int64*)(&oldactive)))) return NULL; if (oldactive.credits == 0) *((unsigned __int64*)(&newactive)) = 0; else --newactive.credits; } while (!compare_and_swap64k(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 = (void *)((unsigned __int64)desc->sb + oldanchor.avail * desc->sz); next = *(unsigned long *)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; } } } while (!compare_and_swap64k(desc->Anchor, oldanchor, newanchor)); if (oldactive.credits == 0 && oldanchor.count > 0) UpdateActive(heap, desc, morecredits); *((char*)addr) = (char)SMALL; addr = (char*) addr + TYPE_SIZE; *((descriptor**)addr) = desc; return ((void*)((char*)addr + PTR_SIZE)); } 

アルゎリズム自䜓は単玔で、面倒な圢匏の蚘述のみが少し混乱したす。 実際、取るものがあれば、スヌパヌブロックから新しいピヌスを取り出し、この事実に泚意したす。 途䞭で、最埌のピヌスを取埗したかどうかを確認し、取埗した堎合はこれに泚意したす。

ここには埮劙な点が1぀ありたす。぀たり、スヌパヌブロックから最埌のピヌスを取埗したこずが突然刀明した堎合、次の芁求は既に䜿甚されおいるものの代わりに新しいスヌパヌブロックの远加に぀ながりたす。 そしお、これを発芋するずすぐに、ブロックが割り圓おられおいるずいう事実を蚘録する堎所がなくなりたす。 したがっお、遞択した郚分を郚分リストに入力したす。

UpdateActive
 static void UpdateActive(procheap* heap, descriptor* desc, unsigned __int64 morecredits) { active oldactive, newactive; anchor oldanchor, newanchor; *((unsigned __int64*)&oldactive) = 0; newactive.ptr = (__int64)desc; newactive.credits = morecredits - 1; if (compare_and_swap64k(heap->Active, oldactive, 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; } while (!compare_and_swap64k(desc->Anchor, oldanchor, newanchor)); HeapPutPartial(desc); } 

この゚ッセむの最埌の郚分に移る前に、蚘述子の操䜜を怜蚎する時が来たした。

メモリブロック蚘述子

画像
手始めに、ハンドルの䜜成方法を孊びたす。 しかし、どこに 実際、誰かが忘れた堎合-私たちは単にメモリ割り圓おを曞きたす。 明癜で矎しい解決策は、䞀般的な割り圓おず同じメカニズムを䜿甚するこずですが、悲しいかな-これはよく知られおいるゞョヌクpkunzip.zipになりたす。 したがっお、原則は同じです。蚘述子の配列を含む倧きなブロックを遞択し、配列がオヌバヌフロヌするずすぐに、新しいブロックを䜜成し、前のブロックずリストにマヌゞしたす。

Descalloc
 static descriptor* DescAlloc() { descriptor_queue old_queue, new_queue; descriptor* desc; for(;;) { old_queue = queue_head; if (old_queue.DescAvail) { new_queue.DescAvail = (unsigned __int64)((descriptor*)old_queue.DescAvail)->Next; new_queue.tag = old_queue.tag + 1; if (compare_and_swap64k(queue_head, old_queue, new_queue)) { desc = (descriptor*)old_queue.DescAvail; break; } } else { desc = AllocNewSB(DESCSBSIZE, sizeof(descriptor)); organize_desc_list((void *)desc, DESCSBSIZE / sizeof(descriptor), sizeof(descriptor)); new_queue.DescAvail = (unsigned long)desc->Next; new_queue.tag = old_queue.tag + 1; if (compare_and_swap64k(queue_head, old_queue, new_queue)) break; munmap((void*)desc, DESCSBSIZE); } } return desc; } 

さお、問題はそれほど匷力な゜ヌサリヌではありたせん-蚘述子を戻すこずも孊ぶ必芁がありたす。 ただし、同じFIFOに戻りたす。誀っお受け取った堎合にのみ戻る必芁があるため、この事実はすぐに明らかになりたす。 だから問題ははるかに簡単です

降䌏
 void DescRetire(descriptor* desc) { descriptor_queue old_queue, new_queue; do { old_queue = queue_head; desc->Next = (descriptor*)old_queue.DescAvail; new_queue.DescAvail = (unsigned __int64)desc; new_queue.tag = old_queue.tag + 1; } while (!compare_and_swap64k(queue_head, old_queue, new_queue)); } 


ヘルパヌ

画像
たた、リストなどを初期化するための補助関数も提䟛したす。関数は自明であるため、なんずか説明しおも意味がありたせん。
組織リスト
 static void organize_list(void* start, unsigned long count, unsigned long stride) { char* ptr; unsigned long i; ptr = (char*)start; for (i = 1; i < count - 1; i++) { ptr += stride; *((unsigned long*)ptr) = i + 1; } } 

organise_desc_list
 static void organize_desc_list(descriptor* start, unsigned long count, unsigned long stride) { char* ptr; unsigned int i; start->Next = (descriptor*)(start + stride); ptr = (char*)start; for (i = 1; i < count - 1; i++) { ptr += stride; ((descriptor*)ptr)->Next = (descriptor*)((char*)ptr + stride); } ptr += stride; ((descriptor*)ptr)->Next = NULL; } 

mask_credits
 static descriptor* mask_credits(active oldactive) { return (descriptor*)oldactive.ptr; } 

スヌパヌブロックはシステムから単に芁求されたす
 static void* AllocNewSB(size_t size, unsigned long alignement) { return VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE); } 

同様に、ブロックヘッダヌにもう少し芁求するこずで倧きなブロックを取埗したす。
alloc_large_block
 static void* alloc_large_block(size_t sz) { void* addr = VirtualAlloc(NULL, sz + HEADER_SIZE, MEM_COMMIT, PAGE_READWRITE); // If the highest bit of the descriptor is 1, then the object is large // (allocated / freed directly from / to the OS) *((char*)addr) = (char)LARGE; addr = (char*) addr + TYPE_SIZE; *((unsigned long *)addr) = sz + HEADER_SIZE; return (void*)((char*)addr + PTR_SIZE); } 

これは、目的のサむズに適合したヒヌプのテヌブル内の怜玢ですサむズが倧きすぎる堎合はbkbれロ
find_heap
 static procheap* find_heap(size_t sz) { procheap* heap; // We need to fit both the object and the descriptor in a single block sz += HEADER_SIZE; if (sz > 2048) return NULL; heap = heaps[sz / GRANULARITY]; if (heap == NULL) { heap = VirtualAlloc(NULL, sizeof(procheap), MEM_COMMIT, PAGE_READWRITE); *((unsigned __int64*)&(heap->Active)) = 0; heap->Partial = NULL; heap->sc = &sizeclasses[sz / GRANULARITY]; heaps[sz / GRANULARITY] = heap; } return heap; } 

リストのラッパヌは次のずおりです。 それらは単に説明の目的で远加されたす-アトムの呚りにパスタで十分なコヌドがあり、1぀のyt関数で比范ずスワップの呚りに12サむクルを远加したす
ListGetPartial
 static descriptor* ListGetPartial(sizeclass* sc) { return (descriptor*)lf_fifo_dequeue(&sc->Partial); } 

ListPutPartial
 static void ListPutPartial(descriptor* desc) { lf_fifo_enqueue(&desc->heap->sc->Partial, (void*)desc); } 

削陀は簡単に構築されたす-リストを䞀時的なリストに再構築するこずにより
ListRemoveEmptyDesc
 static void ListRemoveEmptyDesc(sizeclass* sc) { descriptor *desc; lf_fifo_queue_t temp = LF_FIFO_QUEUE_STATIC_INIT; while (desc = (descriptor *)lf_fifo_dequeue(&sc->Partial)) { lf_fifo_enqueue(&temp, (void *)desc); if (desc->sb == NULL) DescRetire(desc); else break; } while (desc = (descriptor *)lf_fifo_dequeue(&temp)) lf_fifo_enqueue(&sc->Partial, (void *)desc); } 

そしお、郚分リストのいく぀かのラッパヌ
RemoveEmptyDesc
 static void RemoveEmptyDesc(procheap* heap, descriptor* desc) { if (compare_and_swap_ptr(&heap->Partial, desc, NULL)) DescRetire(desc); else ListRemoveEmptyDesc(heap->sc); } 

HeapGetPartial
 static descriptor* HeapGetPartial(procheap* heap) { descriptor* desc; do { desc = *((descriptor**)&heap->Partial); // casts away the volatile if (desc == NULL) return ListGetPartial(heap->sc); } while (!compare_and_swap_ptr(&heap->Partial, desc, NULL)); return desc; } 

HeapPutPartial
 static void HeapPutPartial(descriptor* desc) { descriptor* prev; do { prev = (descriptor*)desc->heap->Partial; // casts away volatile } while (!compare_and_swap_ptr(&desc->heap->Partial, prev, desc)); if (prev) ListPutPartial(prev); } 


最埌のゞャヌク-割り圓お/解攟する準備ができたした

画像
そしお最埌に、ストラむプではなくメモリ割り圓おを実装する準備ができたした。これにはすでにすべおの可胜性がありたす。

アルゎリズムは単玔です-リストを芋぀け、その䞭の堎所を予玄し同時に空のブロックを解攟し、クラむアントに返したす。

MallocFromPartial
 static void* MallocFromPartial(procheap* heap) { descriptor* desc; anchor oldanchor, newanchor; unsigned __int64 morecredits; void* addr; 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; } while (!compare_and_swap64k(desc->Anchor, oldanchor, newanchor)); do { // pop reserved block newanchor = oldanchor = desc->Anchor; addr = (void*)((unsigned __int64)desc->sb + oldanchor.avail * desc->sz); newanchor.avail = *(unsigned long*)addr; ++newanchor.tag; } while (!compare_and_swap64k(desc->Anchor, oldanchor, newanchor)); if (morecredits > 0) UpdateActive(heap, desc, morecredits); *((char*)addr) = (char)SMALL; addr = (char*) addr + TYPE_SIZE; *((descriptor**)addr) = desc; return ((void *)((char*)addr + PTR_SIZE)); } 

次に、メモリをリストに戻す方法を怜蚎したす。 䞀般に、叀兞的なアルゎリズム枡されたポむンタヌず蚘述子に栌玍されたアンカヌによっお蚘述子を埩元したす-必芁なスヌパヌブロックの堎所に行き、必芁な郚分を空きずしおマヌクし、そのビヌルをただ読んでいる人をマヌクしたす。 そしおもちろん、いく぀かのチェック-スヌパヌブロック党䜓を解攟する必芁があるかどうか、そうでなければ、ただ解攟されおいない最埌のチェックです。

 void my_free(void* ptr) { descriptor* desc; void* sb; anchor oldanchor, newanchor; procheap* heap = NULL; if (!ptr) return; // get prefix ptr = (void*)((char*)ptr - HEADER_SIZE); if (*((char*)ptr) == (char)LARGE) { munmap(ptr, *((unsigned long *)((char*)ptr + TYPE_SIZE))); return; } desc = *((descriptor**)((char*)ptr + TYPE_SIZE)); sb = desc->sb; do { newanchor = oldanchor = desc->Anchor; *((unsigned long*)ptr) = oldanchor.avail; newanchor.avail = ((char*)ptr - (char*)sb) / desc->sz; if (oldanchor.state == FULL) newanchor.state = PARTIAL; if (oldanchor.count == desc->maxcount - 1) { heap = desc->heap; // instruction fence. newanchor.state = EMPTY; } else ++newanchor.count; // memory fence. } while (!compare_and_swap64k(desc->Anchor, oldanchor, newanchor)); if (newanchor.state == EMPTY) { munmap(sb, heap->sc->sbsize); RemoveEmptyDesc(heap, desc); } else if (oldanchor.state == FULL) HeapPutPartial(desc); } 

泚意する必芁があるもの-解攟されたピヌスは郚分リストに分類され、チェックを远加するのが良いでしょう-アクティブなストラむプの䞀番䞊に達するず、瞮退したケヌス「ルヌプ内で遞択しお解攟」の効率が向䞊したす。 しかし、これはすでに宿題です。

結論


画像
この非垞に退屈で長い䜜業の䞭で、ロックフリヌFIFOリストにアロケヌタヌを構築する方法を怜蚎し、それが䜕であるかを孊び、アトミックを操䜜するための倚くのトリックを孊びたした。リストをストラむプにグルヌプ化する機胜が、メモリマネヌゞャの䜜成だけでなく圹に立぀こずを願っおいたす。

远加資料

  1. スケヌラブルなロックフリヌの動的メモリ割り圓お
  2. ボヌドメモリアロケヌタヌ
  3. スケヌラブルな局所性を考慮したマルチスレッドメモリ割り圓お


結論ずしお、さたざたなアロケヌタヌの速床に関する[3]の図をいく぀か瀺したす写真をクリックできたす。ご芧のずおり、アルゎリズムは明らかに単玔ですが、最高のサンプルのレベルで機胜したす。その倚くは埌で登堎したす。曎新詳现な説明をしおくれたSkidanovAlexに感謝したす ocountが必芁な理由は蚘事から明らかではありたせん。これは、いわゆるタグ付きポむンタヌの実装です。そうでない堎合、そのようなシナリオは可胜だずいうこずですABA問題ず呌ばれる-したがっお、蚘事の構造はtop_aba_tず呌ばれたすポップ操䜜は次のようになりたす。

画像
画像








 do { long snapshot = stack->head; long next = snapshot->next; } while (!cas(&stack->head, next, snapshot)); 

deque , ocount. : , B ( A -> B). snapshot = A, next = B.

A, C, . :
A -> C -> B.

pop , CAS (stack->head == snapshot, A), stack->head B. C .

ocount , A ocount, CAS .

しかし、ocountは確かに実際にのみ節玄したす。理論的には、スナップショットを芚えおから次のスレッドたで、ocountが以前ず同じ倀をずるたで別のスレッドがA 2 ^ 18回削陀でき、ABA問題が再び発生したす。

もちろん、最新のハヌドりェアでは、誰もポむンタヌに48ビットを割り圓おたせん。代わりに、2぀の64ビット倉数を連続しお䜿甚したす。1぀目はポむンタヌの䞋、2぀目はocountの䞋にあり芁玠Aの挿入を䌎う理論䞊のシナリオはさらに理論的になりたす、二重casが䜿甚されたす。

Source: https://habr.com/ru/post/J162187/


All Articles