【實戰項目】--- 高併發內存池(TCMalloc)

1.引言:

在生活中,住在山上的人需要下山挑水,如果每次需要用水的時候再下來擔水使用的話非常的浪費時間,那麼換角度思考,如果在家中建造固定大小水池,一次性存滿,那麼既可以節約時間,還可以隨用隨取,相當方便。在計算機內存使用領域,TCMalloc 是 Google 開發的內存分配器,因其高效、實用的特點,在不少項目中都有使用,例如在 Golang 中就使用了類似的算法進行內存分配。它具有現代化內存分配器的基本特徵:對抗內存碎片、在多核處理器能夠解決鎖競爭問題和性能問題,優秀的東西大家總是會去想法設法搞懂它,實現它!!!。

2.簡介

項目環境:Wondows10 VS2013 C/C++

什麼是內存池?
內存池(Memory Pool) 是一種動態內存分配與管理技術。 通常情況下,程序員習慣直接使用 new、delete、malloc、free 等API申請分配和釋放內存,這樣導致的後果是:當程序長時間運行時,由於所申請內存塊的大小不定,頻繁使用時會造成大量的內存碎片從而降低程序和操作系統的性能。內存池則是在真正使用內存之前,先申請分配一大塊內存(內存池)留作備用,當程序員申請內存時,從池中取出一塊動態分配,當程序員釋放內存時,將釋放的內存再放入池內,再次申請池可以 再取出來使用,並儘量與周邊的空閒內存塊合併。若內存池不夠時,則自動擴大內存池,從操作系統中申請更大的內存池

3.如何實現高併發內存池?

現代很多的開發環境都是多核多線程,在申請內存的場景下,必然存在激烈的鎖競爭問題。所以這次我們實現的內存池需要考慮以下幾方面的問題。

  • 1. 內存碎片問題。
  • 2. 性能問題。
  • 3. 多核多線程環境下,鎖競爭問題

就上訴三個問題而言,要解決鎖的競爭,我們就不能在用戶申請輕量級內存(<=64k)時對其加鎖,另外要解決內存碎片問題,在用戶用完內存後,要對內存解釋回收,按需調度,此時設計三個模塊解決上述問題。

  • ThreadCache:線程緩存是每個線程獨有的,用於<=64k的內存分配,線程從這裏申請時,不需要加鎖,且每個線程獨享一個Cache,這就是這個併發線程池高效的地方。
  • CentralCache:中心緩存時所有線程所共享的,ThreadCache是按需從CentraCache索取內存對象,而CentralCache週期性的回收來自ThreadCache中的內存對象,避免一個線程長期佔用太多的內存,而其他內存非常喫緊,達到了內存分配在多個線程中更加均衡的按需調度的目的,CentralCache是存在競爭的,所以這裏有必要加鎖,因爲每次都給ThreadCache足夠用的內存大小,所以並不會經常性向CentralCache索取內存,所以鎖的競爭不會太激烈。
  • PageCache:頁緩存是在CentrCache上面的一層緩存着,存儲的內存是以頁爲單位進行存儲以及分配,CentralCache沒有內存對象時,就申請從PageCache分配一定數量的Page,並切割成定長大小的小塊內存,分配給CentralCache。PageCache會回收CentralCache滿足條件的span對象,並且合併相鄰的頁,組成更大的頁,進而實現解決內存碎片的問題。
    在這裏插入圖片描述

4.如何計算一次申請多少個節點?

用設置的最大除以申請的內存,如果申請的內存越大,就給的越少,相反,申請的越少,就一次性給你512份讓你足夠用,這樣很大程度能夠緩解鎖的競爭問題!!!

	//計算一次申請多少個節點
	static size_t NumMoveSize(size_t size)
	{
		if (size == 0)
			return 0;

		int num = MAX_SIZE / size;
		if (num < 2)
			num = 2;

		if (num > 512)
			num = 512;

		return num;
	}

5.計算一次向系統申請多少頁?

	//計算一次向系統獲取幾個頁
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t npage = num*size;

		npage >>= 12;
		if (npage == 0)
			npage = 1;

		return npage;
	}

6.如何分配定長記錄?

出於最大化內存利用率的目的,我們使用另一種經典的方式,freelist。將 4KB 的內存劃分爲 16 字節的單元,每個單元的前8個字節或者前4個字節作爲節點指針,指向下一個單元。初始化的時候把所有指針指向下一個單元;分配時,從鏈表頭分配一個對象出去;釋放時,插入到鏈表。由於鏈表指針直接分配在待分配內存中,因此不需要額外的內存開銷,而且分配速度也是相當快。

7.如何分配變長記錄?

在這裏插入圖片描述
在這裏把所有的變長記錄進行“取整”,例如分配7字節,就分配8字節,20字節分配32字節,得到多種規格的定長記錄。這裏帶來了內部內存碎片的問題,即分配出去的空間不會被完全利用,有一定浪費。爲了減少內部碎片,分配規則按照 8, 16, 32, 48, 64這樣子來。注意到,這裏並不是簡單地使用2的冪級數,因爲按照2的冪級數,內存碎片會相當嚴重,分配65字節,實際會分配128字節,接近50%的內存碎片。而按照這裏的分配規格,只會分配80字節,一定程度上減輕了問題。

8.TLS(線程局部存儲技術)

概念:線程局部存儲(Thread Local Storage,TLS)用來將數據與一個正在執行的指定線程關聯起來。

進程中的全局變量與函數內定義的靜態(static)變量,是各個線程都可以訪問的共享變量。在一個線程修改的內存內容,對所有線程都生效。這是一個優點也是一個缺點。說它是優點,線程的數據交換變得非常快捷。說它是缺點,一個線程死掉了,其它線程也性命不保; 多個線程訪問共享數據,需要昂貴的同步開銷,也容易造成同步相關的BUG。

如果需要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量(被稱爲static memory local to a thread 線程局部靜態變量),就需要新的機制來實現。這就是TLS。

功能:它主要是爲了避免多個線程同時訪存同一全局變量或者靜態變量時所導致的衝突,尤其是多個線程同時需要修改這一變量時。爲了解決這個問題,我們可以通過TLS機制,爲每一個使用該全局變量的線程都提供一個變量值的副本,每一個線程均可以獨立地改變自己的副本,而不會和其它線程的副本衝突。從線程的角度看,就好像每一個線程都完全擁有該變量。而從全局變量的角度上來看,就好像一個全局變量被克隆成了多份副本,而每一份副本都可以被一個線程獨立地改變

_declspec(thread) static ThreadCache* pThreadCache = nullptr;

9.Span及其管理

在這裏插入圖片描述
span的本意爲跨度,在這裏應用也就是CentralCache的一個內存管理的"代表",管理着CentralCache和PageCache裏面的以頁爲單位的內存對象,它裏面記錄有頁號、頁的數量、對象自由鏈表、自由鏈表對象大小、內存塊對象使用計數這五個非常重要的信息,有了這些信息就可以實現對自由鏈表對象的管理,以及再給ThreadCache都回來之後,usecount爲0時,就可以把span還給PageCache用來對span進行合併,那麼頁號的作用此時也就凸顯了出來,它在PageCache中,如果相鄰的span都回來掛在了相對應的位置,那麼我們就可以對相對小的span進行合併成大的span,假如此時來申請大的span時,就可以輕鬆的給CentralCache使用,從根源上解決了後置內存碎片的問題!!!
在這裏插入圖片描述
內存申請切割span
內存釋放合併span
當初始時只有 128 Page 的 Span,如果要分配 1 個 Page 的 Span,就把這個 Span 分裂成兩個,1 + 127,把127再記錄下來。對於 Span 的回收,需要考慮Span的合併問題,否則在分配回收多次之後,就只剩下很小的 Span 了,也就是帶來了外部碎片 問題。爲此,釋放 Span 時,需要將前後的空閒 Span 進行合併,當然,前提是它們的 Page 要連續。

10.頁和Span的映射關係

在這裏插入圖片描述
CentralCache中沒有非空的span時,則將空的span鏈在一起,向Page Cache申請一個Span對象,span對象中是一些以頁爲單位的內存,切成需要的內存大小,並鏈接起來,掛到span中Span 中記錄了起始 Page,也就是知道了從 Span 到 Page 的映射,那麼我們只要知道從 Page 到 Span 的映射,就可以知道前後的Span 是什麼了。在這裏我每沒有沿襲TCmalloc裏面用的基數樹原理,而是採用了較爲簡便的unordered_map來實現頁號和頁的映射關係,從來快速找到頁並判斷它的狀態,從而實現頁的合併。

11.全局內存管理

在這裏插入圖片描述
每個線程都一個線程局部的 ThreadCache,按照不同的規格,維護了對象的鏈表;如果ThreadCache 的對象不夠了,就從 CentralCache 進行批量分配;如果 CentralCache 依然沒有,就從PageCache申請Span;如果 PageCache沒有合適的 Page,就只能從操作系統申請了。在釋放內存的時候,ThreadCache依然遵循批量釋放的策略,對象積累到一定程度就釋放給 CentralCache;CentralCache發現一個 Span的內存完全釋放了,就可以把這個 Span 歸還給PageCache;PageCache發現一批連續的Page都釋放了,就可以歸還給操作系統。

12.拓展和不足

不足:項目中並沒有完全脫離malloc,比如在內存池自身數據結構的管理中,如SpanList中的span等結構,我們還是使用的new Span這樣的操作,new的底層使用的是malloc,所以還不足以替換malloc,因爲們本身沒有完全脫離它。
拓展
①項目中增加一個定長的ObjectPool的對象池,對象池的內存直接使用brk、VirarulAlloc等向系統申請,new Span替換成對象池申請內存。這樣就完全脫離的malloc,就可以替換掉malloc。
②在項目中使用了unordered_map來映射頁id和span的關係,進而實現頁的查找,而unordered_map的效率並非最優,我們可以使用基數樹技術來改進!!!

13.源碼

點擊查看源碼(https://github.com/SJRLL/neicunyinhang)

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