欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++高并發(fā)內(nèi)存池的整體設(shè)計(jì)和實(shí)現(xiàn)思路

 更新時(shí)間:2021年07月13日 10:35:39   作者:Moua  
這篇文章主要介紹了C++高并發(fā)內(nèi)存池的整體設(shè)計(jì)和實(shí)現(xiàn)思路詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

一、整體設(shè)計(jì)

1、需求分析

池化技術(shù)是計(jì)算機(jī)中的一種設(shè)計(jì)模式,內(nèi)存池是常見的池化技術(shù)之一,它能夠有效的提高內(nèi)存的申請和釋放效率以及內(nèi)存碎片等問題,但是傳統(tǒng)的內(nèi)存池也存在一定的缺陷,高并發(fā)內(nèi)存池相對于普通的內(nèi)存池它有自己的獨(dú)特之處,解決了傳統(tǒng)內(nèi)存池存在的一些問題。

附:實(shí)現(xiàn)一個(gè)內(nèi)存池管理的類方法

1)直接使用new/delete、malloc/free存在的問題

new/delete用于c++中動(dòng)態(tài)內(nèi)存管理而malloc/free在c++和c中都可以使用,本質(zhì)上new/delete底層封裝了malloc/free。無論是上面的那種內(nèi)存管理方式,都存在以下兩個(gè)問題:

  • 效率問題:頻繁的在堆上申請和釋放內(nèi)存必然需要大量時(shí)間,降低了程序的運(yùn)行效率。對于一個(gè)需要頻繁申請和釋放內(nèi)存的程序來說,頻繁調(diào)用new/malloc申請內(nèi)存,delete/free釋放內(nèi)存都需要花費(fèi)系統(tǒng)時(shí)間,頻繁的調(diào)用必然會(huì)降低程序的運(yùn)行效率。
  • 內(nèi)存碎片:經(jīng)常申請小塊內(nèi)存,會(huì)將物理內(nèi)存“切”得很碎,導(dǎo)致內(nèi)存碎片。申請內(nèi)存的順序并不是釋放內(nèi)存的順序,因此頻繁申請小塊內(nèi)存必然會(huì)導(dǎo)致內(nèi)存碎片,造成“有內(nèi)存但是申請不到大塊內(nèi)存”的現(xiàn)象。

2)普通內(nèi)存池的優(yōu)點(diǎn)和缺點(diǎn)

針對直接使用new/delete、malloc/free存在的問題,普通內(nèi)存池的設(shè)計(jì)思路是:預(yù)先開辟一塊大內(nèi)存,程序需要內(nèi)存時(shí)直接從該大塊內(nèi)存中“拿”一塊,提高申請和釋放內(nèi)存的效率,同時(shí)直接分配大塊內(nèi)存還減少了內(nèi)存碎片問題。

優(yōu)點(diǎn):申請和釋放內(nèi)存的效率有所提高;一定程度上解決了內(nèi)存碎片問題。

缺點(diǎn):多線程并發(fā)場景下申請和釋放內(nèi)存存在鎖競爭問題造成申請和釋放內(nèi)存的效率降低。

3)高并發(fā)內(nèi)存池要解決的問題

基于以上原因,設(shè)計(jì)高并發(fā)內(nèi)存池需要解決以下三個(gè)問題:

  • 效率問題
  • 內(nèi)存碎片問題
  • 多線程并發(fā)場景下的內(nèi)存釋放和申請的鎖競爭問題。

2、總體設(shè)計(jì)思路

高并發(fā)內(nèi)存池整體框架由以下三部分組成,各部分的功能如下:

  • 線程緩存(thread cache):每個(gè)線程獨(dú)有線程緩存,主要解決多線程下高并發(fā)運(yùn)行場景線程之間的鎖競爭問題。線程緩存模塊可以為線程提供小于64k內(nèi)存的分配,并且多個(gè)線程并發(fā)運(yùn)行不需要加鎖。
  • 中心控制緩存(central control cache):中心控制緩存顧名思義,是高并發(fā)內(nèi)存池的中心結(jié)構(gòu)主要用來控制內(nèi)存的調(diào)度問題。負(fù)責(zé)大塊內(nèi)存切割分配給線程緩存以及回收線程緩存中多余的內(nèi)存進(jìn)行合并歸還給頁緩存,達(dá)到內(nèi)存分配在多個(gè)線程中更均衡的按需調(diào)度的目的,它在整個(gè)項(xiàng)目中起著承上啟下的作用。(注意:這里需要加鎖,當(dāng)多個(gè)線程同時(shí)向中心控制緩存申請或歸還內(nèi)存時(shí)就存在線程安全問題,但是這種情況是極少發(fā)生的,并不會(huì)對程序的效率產(chǎn)生較大的影響,總體來說利大于弊)
  • 頁緩存(page cache):以頁為單位申請內(nèi)存,為中心控制緩存提供大塊內(nèi)存。當(dāng)中心控制緩存中沒有內(nèi)存對象時(shí),可以從page cache中以頁為單位按需獲取大塊內(nèi)存,同時(shí)page cache還會(huì)回收central control cache的內(nèi)存進(jìn)行合并緩解內(nèi)存碎片問題。

3、申請內(nèi)存流程圖

二、詳細(xì)設(shè)計(jì)

1、各個(gè)模塊內(nèi)部結(jié)構(gòu)詳細(xì)剖析

1)thread cache

邏輯結(jié)構(gòu)設(shè)計(jì)

thread cache的主要功能就是為每一個(gè)線程提供64K以下大小內(nèi)存的申請。為了方便管理,需要提供一種特定的管理模式,來保存未分配的內(nèi)存以及被釋放回來的內(nèi)存,以方便內(nèi)存的二次利用。這里的管理通常采用將不同大小的內(nèi)存映射在哈希表中,鏈接起來。而內(nèi)存分配的最小單位是字節(jié),64k = 1024*64Byte如果按照一個(gè)字節(jié)一個(gè)字節(jié)的管理方式進(jìn)行管理,至少也得需要1024*64大小的哈希表對不同大小的內(nèi)存進(jìn)行映射。為了減少哈希表長度,這里采用按一定數(shù)字對齊的方式進(jìn)行內(nèi)存分配,將浪費(fèi)率保持在1%~12%之間。具體結(jié)構(gòu)如下:

具體說明如下:

  • 使用數(shù)組進(jìn)行哈希映射,每一個(gè)位置存放的是一個(gè)鏈表freelists,該鏈表的作用是將相同大小的內(nèi)存對象鏈接起來方便管理。
  • 每個(gè)數(shù)組元素鏈接的都是不同大小的內(nèi)存對象。
  • 第一個(gè)元素表示對齊數(shù)是8,第2個(gè)是16....依次類推。對齊數(shù)表示在上一個(gè)對齊數(shù)和這個(gè)對齊數(shù)之間的大小的內(nèi)存都映射在這個(gè)位置,即要申請1字節(jié)或者7字節(jié)的內(nèi)存都在索引位置為0出找8字節(jié)大小的內(nèi)存,要申請9~16字節(jié)大小的內(nèi)存都在索引為1的位置找16字節(jié)的內(nèi)存對象。
  • 通過上面的分析,可以看出如果進(jìn)行8字節(jié)對齊,最多會(huì)浪費(fèi)7字節(jié)的內(nèi)存(實(shí)際申請1字節(jié)內(nèi)存,返回的是8字節(jié)大小的內(nèi)存對象),將這種現(xiàn)象稱為內(nèi)存碎片浪費(fèi)。
  • 為了將內(nèi)存碎片浪費(fèi)保持在12%以下,也就是說最多容忍有12%的內(nèi)存浪費(fèi),這里使用不同的對齊數(shù)進(jìn)行對齊。
  • 0~128采用8字節(jié)對齊,129~1024采用16字節(jié)對齊,1025~8*1024采用128字節(jié)對齊,8*1024~64*1024采用1024字節(jié)對齊;內(nèi)存碎片浪費(fèi)率分別為:1/8,129/136,1025/1032,8102/8199均在12%左右。同時(shí),8字節(jié)對齊時(shí)需要[0,15]共16個(gè)哈希映射;16字節(jié)對齊需要[16,71]共56個(gè)哈希映射;128字節(jié)對齊需要[72,127]共56個(gè)哈希映射;1024字節(jié)對齊需要[128,184]共56個(gè)哈希映射。
  • 哈希映射的結(jié)構(gòu)如下:

如何保證每個(gè)線程獨(dú)有?

TLS(Thread Local Stirage)

大于64k的內(nèi)存如何申請?

當(dāng)thread cache中申請的內(nèi)存大于64K時(shí),直接向page cache申請。但是page cache中最大也只能申請128頁的內(nèi)存,所以當(dāng)thread cache申請的內(nèi)存大于128頁時(shí)page cache中會(huì)自動(dòng)給thread cache在系統(tǒng)內(nèi)存中申請。

2)central control cache

central control cache作為thread cache和page cache的溝通橋梁,起到承上啟下的作用。它需要向thread cache提供切割好的小塊內(nèi)存,同時(shí)他還需要回收thread cache中的多余內(nèi)存進(jìn)行合并,在分配給其他其他thread cache使用,起到資源調(diào)度的作用。它的結(jié)構(gòu)如下:

具體說明如下:

  • central control cache的結(jié)構(gòu)依然是一個(gè)數(shù)組,他保存的是span類型的對象。
  • span是用來管理一塊內(nèi)存的,它里邊包含了一個(gè)freelist鏈表,用于將大塊內(nèi)存切割成指定大小的小塊內(nèi)存鏈接到freelist中,當(dāng)thread cache需要內(nèi)存時(shí)直接將切割好的內(nèi)存給thread cache。
  • 開始時(shí),每個(gè)數(shù)組索引位置都是空的,當(dāng)thread cache申請內(nèi)存時(shí),spanList數(shù)組會(huì)向page cache申請一大塊內(nèi)存進(jìn)行切割后掛在list中。當(dāng)該快內(nèi)存使用完,會(huì)繼續(xù)申請新的內(nèi)存,因此就存在多個(gè)span鏈接的情況。前邊span存在對象是因?yàn)橛锌赡芎筮呉呀?jīng)申請好內(nèi)存了前邊的內(nèi)存也釋放回來了。
  • 當(dāng)某一個(gè)span的全部內(nèi)存都還回來時(shí),central control cache會(huì)再次將這塊內(nèi)存合并,在歸還到page cache中。
  • 當(dāng)central control cache為空時(shí),向page cache申請內(nèi)存,每次至少申請一頁,并且必須以頁為單位進(jìn)行申請(這里的頁大小由我們自己決定,這里采用4K)。

這里需要注意的是,thread cache可能會(huì)有多個(gè),但是central control cache只有一個(gè),要讓多個(gè)thread cache對象訪問一個(gè)central control cache對象,這里的central control cache需要設(shè)計(jì)成單例模式。

3)page cache

page cache是以頁為單位進(jìn)行內(nèi)存管理的,它是將不同頁數(shù)的內(nèi)存利用哈希進(jìn)行映射,最多映射128頁內(nèi)存,具體結(jié)構(gòu)如下:

page Cache申請和釋放內(nèi)存流程:

  • 當(dāng)central control cache向page cache申請內(nèi)存時(shí),比如要申請8頁的內(nèi)存,它會(huì)先在span大小為8的位置找,如果沒有就繼續(xù)找9 10...128,那個(gè)有就從那個(gè)中切割8頁。
  • 例如,走到54時(shí)才有內(nèi)存,就從54處切8頁返回給central control cache,將剩余的54-846頁掛在46頁處。
  • 當(dāng)page cache中沒有內(nèi)存時(shí),它直接申請一個(gè)128頁的內(nèi)存掛在128位置。當(dāng)central control cache申請內(nèi)存時(shí)再從128頁切。

2、設(shè)計(jì)細(xì)節(jié)

1)thread cache

根據(jù)申請內(nèi)存大小計(jì)算對應(yīng)的_freelists索引

  • 1~8都映射在索引為0處,9~16都在索引為2處......
  • 因此以8字節(jié)對齊時(shí),可以表示為:((size + (2^3 - 1)) >> 3) - 1;
  • 如果申請的內(nèi)存為129,索引如何計(jì)算?
  • 首先前128字節(jié)是按照8字節(jié)對齊的,因此:((129-128)+(2^4-1))>>4)-1 + 16
  • 上式中16表示索引為0~15的16個(gè)位置以8字節(jié)對齊。

代碼實(shí)現(xiàn):

//根據(jù)內(nèi)存大小和對齊數(shù)計(jì)算對應(yīng)下標(biāo)
static inline size_t _Intex(size_t size, size_t alignmentShift)
{
	//alignmentShift表示對齊數(shù)的位數(shù),例如對齊數(shù)為8 = 2^3時(shí),aligmentShift = 3
	//這樣可以將除法轉(zhuǎn)化成>>運(yùn)算,提高運(yùn)算效率
	return ((size + (1 << alignmentShift) - 1) >> alignmentShift) - 1;
}
//根據(jù)內(nèi)存大小,計(jì)算對應(yīng)的下標(biāo)
static inline size_t Index(size_t size)
{
	assert(size <= THREAD_MAX_SIZE);
 
	//每個(gè)對齊數(shù)對應(yīng)的索引個(gè)數(shù),分別表示8 16 128 1024字節(jié)對齊
	int groupArray[4] = {16,56,56,56};
 
	if (size <= 128)
	{
		//8字節(jié)對齊
		return _Intex(size, 3) + groupArray[0];
	}
	else if (size <= 1024)
	{
		//16字節(jié)對齊
		return _Intex(size, 4) + groupArray[1];
	}
	else if (size <= 8192)
	{
		//128字節(jié)對齊
		return _Intex(size, 7) + groupArray[2];
	}
	else if (size <= 65536)
	{
		//1024字節(jié)對齊
		return _Intex(size, 10) + groupArray[3];
	}
 
	assert(false);
	return -1;
}

freelist向中心緩存申請內(nèi)存時(shí)需要對申請的內(nèi)存大小進(jìn)行對齊

首先,需要申請的內(nèi)存大小不夠?qū)R數(shù)時(shí)都需要進(jìn)行向上對齊。即,要申請的內(nèi)存大小為1字節(jié)時(shí)需要對齊到8字節(jié)。如何對齊?不進(jìn)行對齊可以嗎?

首先,不進(jìn)行對齊也可以計(jì)算出freelist索引,當(dāng)?shù)谝淮紊暾垉?nèi)存時(shí),freelist的索引位置切割后的內(nèi)存大小就是實(shí)際申請的內(nèi)存大小,并沒有進(jìn)行對齊,造成內(nèi)存管理混亂。對齊方式如下:

  • 對齊數(shù)分別為8 = 2^3; 16 = 2^4 ; 128 = 2^7 ; 1024 = 2^10,轉(zhuǎn)化成二進(jìn)制后只有1個(gè)1.
  • 在對齊區(qū)間內(nèi),所有數(shù)+對齊數(shù)-1后一定是大于等于當(dāng)前區(qū)間的最大值且小于下一個(gè)相鄰區(qū)間的最大值。
  • 因此,size + 對齊數(shù) - 1如果是8字節(jié)對齊只需將低3位變?yōu)?,如果是16字節(jié)對齊將低3位變?yōu)?......
  • 例如:size = 2時(shí),對齊數(shù)為8;則size + 8 - 1 = 9,轉(zhuǎn)為而進(jìn)制位1001,將低三位變?yōu)?后為1000,轉(zhuǎn)為十進(jìn)制就是對齊數(shù)8.

代碼表示如下:alignment表示對齊數(shù)

(size + alignment - 1) & ~(alignment - 1);

注意:向這些小函數(shù),定義成inline可以減少壓棧開銷。 ‘

如何將小塊內(nèi)存對象“掛在”freelist鏈表中

哈哈,前邊已經(jīng)為這里做好鋪墊了。前邊規(guī)定單個(gè)對象大小最小為8字節(jié),32位系統(tǒng)下一個(gè)指針的大小為4字節(jié),64位機(jī)器下一個(gè)指針的大小為8字節(jié)。前邊我們規(guī)定單個(gè)對象最小大小為8字節(jié)就是為了無論是在32位系統(tǒng)下還是在64位系統(tǒng)下,都可以保存一個(gè)指針將小塊對象鏈接起來。那么,如何使用一小塊內(nèi)存保存指針?

直接在內(nèi)存的前4/8個(gè)字節(jié)將下一塊內(nèi)存的地址保存,取內(nèi)存時(shí)直接對該內(nèi)存解引用就可以取出地址。

訪問:*(void**)(mem)

每次從freelist中取內(nèi)存或者歸還內(nèi)存時(shí),直接進(jìn)行頭插或頭刪即可。

從central control cache中申請內(nèi)存,一次申請多少合適呢?

這里的思路是采用“慢啟動(dòng)”的方式申請,即第一次申請申請一個(gè),第二次申請2個(gè)....當(dāng)達(dá)到一定大?。?12個(gè))時(shí)不再增加。這樣做的好處是,第一次申請給的數(shù)量少可以防止某些線程只需要一個(gè)多給造成浪費(fèi),后邊給的多可以減少從central control cache的次數(shù)從而提高效率。

當(dāng)使用慢啟動(dòng)得到的期望內(nèi)存對象個(gè)數(shù)大于當(dāng)前central control cache中內(nèi)存對象的個(gè)數(shù)時(shí),有多少給多少。因?yàn)?,?shí)際上目前只需要一個(gè),我們多申請了不夠,那就有多少給多少。當(dāng)一個(gè)都沒有的時(shí)候才會(huì)去page cache申請。

什么時(shí)候thread cache將內(nèi)存還給central controlcache?

當(dāng)一個(gè)線程將內(nèi)存還給thread cache時(shí),會(huì)去判斷對應(yīng)的_freelist的對應(yīng)位置是否有太多的內(nèi)存還回來(thread cache中內(nèi)存對象的大小大于等于最個(gè)數(shù)的時(shí)候,就向central control cache還)。

2)Central Control Cache

SpanList結(jié)構(gòu)

SpanList在central control cache中最重要的作用就是對大塊內(nèi)存管理,它存儲(chǔ)的是一個(gè)個(gè)span類的對象,使用鏈表進(jìn)行管理。結(jié)構(gòu)如下:

也就是說,SpanList本質(zhì)上就是一個(gè)span鏈表。這里考慮到后邊歸還內(nèi)存需要找到對應(yīng)頁歸還,方便插入,這里將spanlist設(shè)置成雙向帶頭循環(huán)鏈表。

Span結(jié)構(gòu)

Span存儲(chǔ)的是大塊內(nèi)存的信息,陪SpanList共同管理大塊內(nèi)存,它的內(nèi)存單位是頁(4K)。它的結(jié)構(gòu)實(shí)際上就是一個(gè)個(gè)size大小的對象鏈接起來的鏈表。它同時(shí)也作為SpanList的節(jié)點(diǎn),spanList是雙向循環(huán)鏈表,因此span中還有next和prev指針。

struct Span
{
PageID _pageId = 0; // 頁號
size_t _n = 0; // 頁的數(shù)量

Span* _next = nullptr;
Span* _prev = nullptr;

void* _list = nullptr; // 大塊內(nèi)存切小鏈接起來,這樣回收回來的內(nèi)存也方便鏈接
size_t _usecount = 0; // 使用計(jì)數(shù),==0 說明所有對象都回來了

size_t _objsize = 0; // 切出來的單個(gè)對象的大小
};

當(dāng)spanList中沒有內(nèi)存時(shí)需要向PageCache申請內(nèi)存,一次申請多少合適呢?

根據(jù)申請的對象的大小分配內(nèi)存,也就是說單個(gè)對象大小越小分配的頁數(shù)越少,單個(gè)對象的大小越大分配到的內(nèi)存越多。如何衡量多少?

這里我們是通過thread cache中從central control cache中獲取的內(nèi)存對象的個(gè)數(shù)的上限來確定。也就是說,個(gè)數(shù)的上限*內(nèi)存對象的大小就是我們要申請的內(nèi)存的大小。在右移12位(1頁)就是需要申請的頁數(shù)。

//計(jì)算申請多少頁內(nèi)存
static inline size_t NumMovePage(size_t memSize)
{
	//計(jì)算thread cache最多申請多少個(gè)對象,這里就給多少個(gè)對象
	size_t num = NumMoveSize(memSize);
	//此時(shí)的nPage表示的是獲取的內(nèi)存大小
	size_t nPage = num*memSize;
	//當(dāng)npage右移是PAGE_SHIFT時(shí)表示除2的PAGE_SHIFT次方,表示的就是頁數(shù)
	nPage >>= PAGE_SHIFT;
 
	//最少給一頁(體現(xiàn)了按頁申請的原則)
	if (nPage == 0)
		nPage = 1;
 
	return nPage;
}

向central control cache申請一塊內(nèi)存,切割時(shí)如果最后產(chǎn)生一個(gè)碎片(不夠一個(gè)對象大小的內(nèi)存)如何處理?

一旦產(chǎn)生這種情況,最后的碎片內(nèi)存只能丟棄不使用。但是對于我們的程序來說是不會(huì)產(chǎn)生的,因?yàn)槲覀兠看紊暾堉辽僖豁摚?096可以整除我們所對應(yīng)的任何一個(gè)大小的對象。

central control cache何時(shí)將內(nèi)存還給page cache?

thread cache將多余的內(nèi)存會(huì)還給central control cache中的spanlist對應(yīng)的span,span中有一個(gè)usecount用來統(tǒng)計(jì)該span中有多少個(gè)對象被申請走了,當(dāng)usecount為0時(shí),表示所有對象都還回來了,則將該span還給page cache,合并成更大的span。

3)Page Cache

當(dāng)從一個(gè)大頁切出一個(gè)小頁內(nèi)存時(shí),剩余的內(nèi)存如何掛在對應(yīng)位置?

在Page cache中的span它是沒有切割的,都是一個(gè)整頁,也就是說這里的Span的list并沒有使用到。這里計(jì)算內(nèi)存的地址都是按照頁號計(jì)算的,當(dāng)一個(gè)Span中有多頁內(nèi)存時(shí)保存的是第一頁的內(nèi)存,那么就可以計(jì)算出剩余內(nèi)存和切走內(nèi)存的頁號,設(shè)置相應(yīng)的頁號進(jìn)行映射即可。

從一個(gè)大的Span中切時(shí),采用頭切還是尾切?

Span中如何通過頁號計(jì)算地址?

每一頁大小都是固定的,當(dāng)我們從系統(tǒng)申請一塊內(nèi)存會(huì)返回該內(nèi)存的首地址,申請內(nèi)存時(shí)返回的都是一塊連續(xù)的內(nèi)存,所以我們可以使用內(nèi)存首地址/頁大小的方式計(jì)算出頁號,通過這種方式計(jì)算出來的一大塊內(nèi)存的多個(gè)頁的頁號都是連續(xù)的。

Page Cache向系統(tǒng)申請內(nèi)存

Page Cache向系統(tǒng)申請內(nèi)存時(shí),前邊我們說過每次直接申請128頁的內(nèi)存。這里需要說明的是,我們的項(xiàng)目中不能出現(xiàn)任和STL中的數(shù)據(jù)結(jié)構(gòu)和庫函數(shù),因此這里申請內(nèi)存直接采用系統(tǒng)調(diào)用VirtualAlloc。下面對VirtualAlloc詳細(xì)解釋:

VirtualAlloc是一個(gè)Windows API函數(shù),該函數(shù)的功能是在調(diào)用進(jìn)程的虛地址空間,預(yù)定或者提交一部分頁。簡單點(diǎn)的意思就是申請內(nèi)存空間。

函數(shù)聲明如下:

LPVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的內(nèi)存區(qū)域的地址

DWORD dwSize, // 分配的大小

DWORD flAllocationType, // 分配的類型

DWORD flProtect // 該內(nèi)存的初始保護(hù)屬性

};

參數(shù)說明:

  • LPVOID lpAddress, 分配內(nèi)存區(qū)域的地址。當(dāng)你使用VirtualAlloc來提交一塊以前保留的內(nèi)存塊的時(shí)候,lpAddress參數(shù)可以用來識(shí)別以前保留的內(nèi)存塊。如果這個(gè)參數(shù)是NULL,系統(tǒng)將會(huì)決定分配內(nèi)存區(qū)域的位置,并且按64-KB向上取整(roundup)。
  • SIZE_T dwSize, 要分配或者保留的區(qū)域的大小。這個(gè)參數(shù)以字節(jié)為單位,而不是頁,系統(tǒng)會(huì)根據(jù)這個(gè)大小一直分配到下頁的邊界DWORD
  • flAllocationType, 分配類型 ,你可以指定或者合并以下標(biāo)志:MEM_COMMIT,MEM_RESERVE和MEM_TOP_DOWN。
  • DWORD flProtect 指定了被分配區(qū)域的訪問保護(hù)方式

注:PageCache中有一個(gè)map用來存儲(chǔ)pageId和Span的映射。在釋放內(nèi)存時(shí),通過memSize計(jì)算出pageId,在通過PageId在map中查找對應(yīng)的Span從而就可以獲得單個(gè)對象的大小,在根據(jù)單個(gè)對象的大小確定是要將內(nèi)存還給page cache還是還給central control cache。

central control cache釋放回來的內(nèi)存如何合并成大內(nèi)存?

通過span中的頁號查找前一頁和后一頁,判斷前一頁和后一頁是否空閑(沒有被申請的內(nèi)存),如果空閑就進(jìn)行和并,合并完后重新在map中進(jìn)行映射。

注意:將PageCache和CentralControlCache設(shè)置成單例模式,因?yàn)槎鄠€(gè)線程對同時(shí)使用一個(gè)page cache和central control cache進(jìn)行內(nèi)存管理。

單例模式簡單介紹

  • 單例模式,顧名思義只能創(chuàng)建一個(gè)實(shí)例。
  • 有兩種實(shí)現(xiàn)方式:懶漢實(shí)現(xiàn)和餓漢實(shí)現(xiàn)
  • 做法:將構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù)定義成私有且不能默認(rèn)生成,防止在類外構(gòu)造對象;定義一個(gè)本身類型的成員,在類中構(gòu)造一個(gè)對象,提供接口供外部調(diào)用。

4)加鎖問題

  • 在central control cache和page cache中都存在多個(gè)線程訪問同一臨界資源的情況,因此需要加鎖。
  • 在central control cache中,不同線程只要訪問的不是同一個(gè)大小的內(nèi)存對象,則就不需要加鎖,可以提高程序的運(yùn)行效率(加鎖后就有可能導(dǎo)致線程掛起等待),也就是說central control cache中是“桶鎖”。需要改freelist那個(gè)位置的內(nèi)存,就對那個(gè)加鎖。
  • page cache中,需要對申請和合并內(nèi)存進(jìn)行加鎖。
  • 這里我們統(tǒng)一使用互斥鎖。

注意:使用map進(jìn)行映射,雖然說我們對pagecache進(jìn)行了加鎖,不會(huì)早成寫數(shù)據(jù)的沖突,但是我們還向外提供了查找的接口,就有可能導(dǎo)致一個(gè)線程在向map中寫而另一個(gè)線程又查找,出現(xiàn)線程安全問題,但是如果給查找位置加鎖,這個(gè)接口會(huì)被頻繁的調(diào)用,造成性能的損失。而在tcmalloc中采用基數(shù)樹來存儲(chǔ)pageId和span的映射關(guān)系,從而提高效率。

附:基數(shù)樹

三、測試

1、單元測試

void func1()
{
	for (size_t i = 0; i < 10; ++i)
	{
		hcAlloc(17);
	}
}
 
void func2()
{
	for (size_t i = 0; i < 20; ++i)
	{
		hcAlloc(5);
	}
}
 
//測試多線程
void TestThreads()
{
	std::thread t1(func1);
	std::thread t2(func2);
 
 
	t1.join();
	t2.join();
}
 
//計(jì)算索引
void TestSizeClass()
{
	cout << SizeClass::Index(1035) << endl;
	cout << SizeClass::Index(1025) << endl;
	cout << SizeClass::Index(1024) << endl;
}
 
//申請內(nèi)存
void TestConcurrentAlloc()
{
	void* ptr0 = hcAlloc(5);
	void* ptr1 = hcAlloc(8);
	void* ptr2 = hcAlloc(8);
	void* ptr3 = hcAlloc(8);
 
	hcFree(ptr1);
	hcFree(ptr2);
	hcFree(ptr3);
}
 
//大塊內(nèi)存的申請
void TestBigMemory()
{
	void* ptr1 = hcAlloc(65 * 1024);
	hcFree(ptr1);
 
	void* ptr2 = hcAlloc(129 * 4 * 1024);
	hcFree(ptr2);
}
 
//int main()
//{
//	//TestBigMemory();
//
//	//TestObjectPool();
//	//TestThreads();
//	//TestSizeClass();
//	//TestConcurrentAlloc();
//
//	return 0;
//}

2、性能測試

void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	//創(chuàng)建nworks個(gè)線程
	std::vector<std::thread> vthread(nworks);
	size_t malloc_costtime = 0;
	size_t free_costtime = 0;
 
	//每個(gè)線程循環(huán)依次
	for (size_t k = 0; k < nworks; ++k)
	{
		//鋪貨k
		vthread[k] = std::thread([&, k]() {
			std::vector<void*> v;
			v.reserve(ntimes);
 
			//執(zhí)行rounds輪次
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				//每輪次執(zhí)行ntimes次
				for (size_t i = 0; i < ntimes; i++)
				{
					v.push_back(malloc(16));
				}
				size_t end1 = clock();
 
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					free(v[i]);
				}
				size_t end2 = clock();
				v.clear();
 
				malloc_costtime += end1 - begin1;
				free_costtime += end2 - begin2;
			}
		});
	}
 
	for (auto& t : vthread)
	{
		t.join();
	}
 
	printf("%u個(gè)線程并發(fā)執(zhí)行%u輪次,每輪次malloc %u次: 花費(fèi):%u ms\n",
		nworks, rounds, ntimes, malloc_costtime);
 
	printf("%u個(gè)線程并發(fā)執(zhí)行%u輪次,每輪次free %u次: 花費(fèi):%u ms\n",
		nworks, rounds, ntimes, free_costtime);
 
	printf("%u個(gè)線程并發(fā)malloc&free %u次,總計(jì)花費(fèi):%u ms\n",
		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}
 
 
// 單輪次申請釋放次數(shù) 線程數(shù) 輪次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
	std::vector<std::thread> vthread(nworks);
	size_t malloc_costtime = 0;
	size_t free_costtime = 0;
 
	for (size_t k = 0; k < nworks; ++k)
	{
		vthread[k] = std::thread([&]() {
			std::vector<void*> v;
			v.reserve(ntimes);
 
			for (size_t j = 0; j < rounds; ++j)
			{
				size_t begin1 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					v.push_back(hcAlloc(16));
				}
				size_t end1 = clock();
 
				size_t begin2 = clock();
				for (size_t i = 0; i < ntimes; i++)
				{
					hcFree(v[i]);
				}
				size_t end2 = clock();
				v.clear();
 
				malloc_costtime += end1 - begin1;
				free_costtime += end2 - begin2;
			}
		});
	}
 
	for (auto& t : vthread)
	{
		t.join();
	}
 
	printf("%u個(gè)線程并發(fā)執(zhí)行%u輪次,每輪次concurrent alloc %u次: 花費(fèi):%u ms\n",
		nworks, rounds, ntimes, malloc_costtime);
 
	printf("%u個(gè)線程并發(fā)執(zhí)行%u輪次,每輪次concurrent dealloc %u次: 花費(fèi):%u ms\n",
		nworks, rounds, ntimes, free_costtime);
 
	printf("%u個(gè)線程并發(fā)concurrent alloc&dealloc %u次,總計(jì)花費(fèi):%u ms\n",
		nworks, nworks*rounds*ntimes, malloc_costtime + free_costtime);
}
 
int main()
{
	cout << "==========================================================" << endl;
	BenchmarkMalloc(100000, 4, 10);
	cout << endl << endl;
 
	BenchmarkConcurrentMalloc(100000, 4, 10);
	cout << "==========================================================" << endl;
 
	return 0;
}

結(jié)果比較

附1:完整代碼

到此這篇關(guān)于C++高并發(fā)內(nèi)存池的整體設(shè)計(jì)和實(shí)現(xiàn)思路的文章就介紹到這了,更多相關(guān)C++高并發(fā)內(nèi)存池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Objective-C的內(nèi)省(Introspection)用法小結(jié)

    Objective-C的內(nèi)省(Introspection)用法小結(jié)

    這篇文章主要介紹了Objective-C的內(nèi)省(Introspection)用法,這是面向?qū)ο笳Z言和環(huán)境的一個(gè)強(qiáng)大特性,需要的朋友可以參考下
    2014-07-07
  • 詳解C/C++ Linux出錯(cuò)處理函數(shù)(strerror與perror)的使用

    詳解C/C++ Linux出錯(cuò)處理函數(shù)(strerror與perror)的使用

    我們知道,系統(tǒng)函數(shù)調(diào)用不能保證每次都成功,必須進(jìn)行出錯(cuò)處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。本文主要為大家介紹兩個(gè)出錯(cuò)處理函數(shù)(strerror、perror)的使用,需要的可以參考一下
    2023-01-01
  • C++內(nèi)存分布及用法

    C++內(nèi)存分布及用法

    這篇文章主要介紹了C++內(nèi)存分布及用法,從內(nèi)存的基礎(chǔ)概念到內(nèi)存分配進(jìn)行了講解,內(nèi)存是我們開發(fā)中最重要的一部分,往往邏輯上的錯(cuò)誤就會(huì)造成內(nèi)存泄漏,導(dǎo)致程序無法運(yùn)行,下面我們就來了解文章對該內(nèi)容的詳細(xì)介紹
    2021-12-12
  • 使用C語言實(shí)現(xiàn)學(xué)生成績管理系統(tǒng)

    使用C語言實(shí)現(xiàn)學(xué)生成績管理系統(tǒng)

    這篇文章主要介紹了使用C語言實(shí)現(xiàn)學(xué)生成績管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • C++設(shè)計(jì)模式之享元模式

    C++設(shè)計(jì)模式之享元模式

    這篇文章主要介紹了C++設(shè)計(jì)模式之享元模式,本文講解了什么是享元模式、享元模式代碼實(shí)例、享元模式的優(yōu)點(diǎn)等內(nèi)容,需要的朋友可以參考下
    2014-10-10
  • C++使用jsoncpp解析json的方法示例

    C++使用jsoncpp解析json的方法示例

    這篇文章主要介紹了C++使用jsoncpp解析json的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • C語言中結(jié)構(gòu)體和共用體實(shí)例教程

    C語言中結(jié)構(gòu)體和共用體實(shí)例教程

    這篇文章主要給大家介紹了關(guān)于C語言中結(jié)構(gòu)體和共用體的相關(guān)資料,結(jié)構(gòu)體是一種自定義的復(fù)合數(shù)據(jù)類型,共用體也叫聯(lián)合體,使幾個(gè)不同類型的變量共占一段內(nèi)存(相互覆蓋),需要的朋友可以參考下
    2021-06-06
  • Qt線程QThread開啟和安全退出的實(shí)現(xiàn)

    Qt線程QThread開啟和安全退出的實(shí)現(xiàn)

    本文主要介紹了Qt線程QThread開啟和安全退出的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • C++讀寫Excel的實(shí)現(xiàn)方法詳解

    C++讀寫Excel的實(shí)現(xiàn)方法詳解

    本篇文章是對C++讀寫Excel的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-05-05
  • 簡單分析C語言中指針數(shù)組與數(shù)組指針的區(qū)別

    簡單分析C語言中指針數(shù)組與數(shù)組指針的區(qū)別

    這篇文章主要介紹了C語言中指針數(shù)組與數(shù)組指針的區(qū)別,是C語言入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下
    2015-11-11

最新評論