溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C++固定內(nèi)存塊分配器相關(guān)知識(shí)點(diǎn)有哪些

發(fā)布時(shí)間:2021-11-30 16:56:24 來源:億速云 閱讀:149 作者:iii 欄目:編程語言

這篇文章主要講解了“C++固定內(nèi)存塊分配器相關(guān)知識(shí)點(diǎn)有哪些”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“C++固定內(nèi)存塊分配器相關(guān)知識(shí)點(diǎn)有哪些”吧!

回收內(nèi)存存儲(chǔ)

內(nèi)存管理模式的基本哲學(xué)是在對(duì)象內(nèi)存分配時(shí)能夠回收內(nèi)存。一旦在內(nèi)存中創(chuàng)建了一個(gè)對(duì)象,它所占用的內(nèi)存就不能被重新分配。同時(shí),內(nèi)存要能夠回收,允許相同類型的對(duì)象重用這部分內(nèi)存。我實(shí)現(xiàn)了一個(gè)名為Allocator的類來展示這些技巧。

當(dāng)應(yīng)用程序使用Allocator類進(jìn)行刪除時(shí),對(duì)象占用的內(nèi)存空間被釋放以備重用,但卻不會(huì)立即釋放給內(nèi)存管理器,這些內(nèi)存保留在就一個(gè)稱之為“釋放列表”的鏈表中,并再次分配給相同類型的對(duì)象。對(duì)每個(gè)內(nèi)存分配的請(qǐng)求,Allocaor類首先檢查“釋放列表”中是否存在待釋放的內(nèi)存。只有“釋放列表”中沒有可用的內(nèi)存空間時(shí)才會(huì)分配新的內(nèi)存。根據(jù)所需的Allocator類的行為,內(nèi)存存儲(chǔ)以三種操作模式使用全局堆內(nèi)存或者靜態(tài)內(nèi)存池。

  • 1.堆內(nèi)存

  • 2.堆內(nèi)存池

  • 3.靜態(tài)內(nèi)存池

堆內(nèi)存 vs. 內(nèi)存池

Allocator類在“釋放列表”為空時(shí),能夠從堆內(nèi)存或者內(nèi)存池中申請(qǐng)新內(nèi)存。如果使用內(nèi)存池,你必須事先確定好對(duì)象的數(shù)量。確保內(nèi)存池足夠容納所有需要使用的對(duì)象。另一方面,使用堆內(nèi)存沒有數(shù)量大小的限制——可以構(gòu)造內(nèi)存允許的盡可能多的對(duì)象。

堆內(nèi)存模式在全局堆內(nèi)存上為對(duì)象分配內(nèi)存。釋放操作將這塊內(nèi)存放入“釋放了列表”以備重用。當(dāng)“釋放列表”為空時(shí),需要在堆內(nèi)存上創(chuàng)建新內(nèi)存。這種方式提供了動(dòng)態(tài)內(nèi)存的分配和釋放,優(yōu)點(diǎn)是內(nèi)存塊可以在運(yùn)行時(shí)動(dòng)態(tài)增加,缺點(diǎn)是內(nèi)存塊創(chuàng)建期間是不確定的,可能創(chuàng)建失敗。

堆內(nèi)存池模式從全局堆內(nèi)存創(chuàng)建一個(gè)內(nèi)存池。當(dāng)Allocator類對(duì)象創(chuàng)建時(shí),使用new操作符創(chuàng)建內(nèi)存池。然后使用內(nèi)存池中的內(nèi)存塊進(jìn)行內(nèi)存分配。

靜態(tài)內(nèi)存池模式使用從靜態(tài)內(nèi)存中分配的內(nèi)存池。靜態(tài)內(nèi)存池由使用者進(jìn)行分配而不是由Allocator對(duì)象進(jìn)行創(chuàng)建。

堆內(nèi)存池模式和靜態(tài)內(nèi)存池模式提供了內(nèi)存操作的連續(xù)使用,因?yàn)閮?nèi)存分配器不需要分配單獨(dú)的內(nèi)存塊。這樣分配內(nèi)存的過程是十分快速且具有確定性的。

類設(shè)計(jì)

類的接口很簡單。Allocate()返回指向內(nèi)存塊的指針,Deallocate()釋放內(nèi)存以備重用。構(gòu)造函數(shù)需要設(shè)置對(duì)象的大小,并且如果使用內(nèi)存池,需要分配內(nèi)存池空間。

類的構(gòu)造函數(shù)中的參數(shù)用于決定內(nèi)存塊分配的位置。size參數(shù)控制固定內(nèi)存塊的大小。objects參數(shù)設(shè)置申請(qǐng)內(nèi)存塊的個(gè)數(shù),其值為0表示從堆內(nèi)存中申請(qǐng)新內(nèi)存塊,非0表示使用內(nèi)存池方式(堆內(nèi)存池或者靜態(tài)內(nèi)存池)分配對(duì)象實(shí)例空間。memory參數(shù)是指向靜態(tài)內(nèi)存的指針。如果memory等于0并且objects非零,Allocator將從堆內(nèi)存中創(chuàng)建一個(gè)內(nèi)存池。靜態(tài)內(nèi)存池內(nèi)存大小必須是size*object字節(jié)。name參數(shù)為內(nèi)存分配器命名,用于收集分配器使用信息。

class Allocator {public:
    Allocator(size_t size, UINT objects=0, CHAR* memory=NULL, const CHAR* name=NULL);
...

下面的例子展示三種分配器模式中的構(gòu)造函數(shù)是如何賦值的。

// Heap blocks mode with unlimited 100 byte blocksAllocator allocatorHeapBlocks(100);// Heap pool mode with 20, 100 byte blocksAllocator allocatorHeapPool(100, 20);// Static pool mode with 20, 100 byte blockschar staticMemoryPool[100 * 20];Allocator allocatorStaticPool(100, 20, staticMemoryPool);

為了簡化靜態(tài)內(nèi)存池方法,提供AllocatorPool<>模板類。模板的***個(gè)參數(shù)設(shè)置申請(qǐng)內(nèi)存對(duì)象類型,第二個(gè)參數(shù)設(shè)置申請(qǐng)對(duì)象的數(shù)量。

// Static pool mode with 20 MyClass sized blocks AllocatorPool<MyClass, 20> allocatorStaticPool2;

Deallocate()將內(nèi)存地址放入“棧”中。這個(gè)“?!钡膶?shí)現(xiàn)方式類似于單項(xiàng)鏈表(“釋放列表”),但是只能添加、移除頭部的對(duì)象,其行為類似棧的特性。使用“?!笔沟梅峙?、釋放操作更為快速,因?yàn)椴恍枰湵肀闅v而只需要壓入和彈出操作。

void* memory1 = allocatorHeapBlocks.Allocate(100);

這樣便在不增加額外存儲(chǔ)的情況下,將內(nèi)存塊鏈接在“釋放列表”中。例如,當(dāng)我們使用全局operate  new時(shí),首先申請(qǐng)內(nèi)存,然后調(diào)用構(gòu)造函數(shù)。delete的過程與此相反,首先調(diào)用析構(gòu)函數(shù),然后釋放掉內(nèi)存。調(diào)用完析構(gòu)函數(shù)后,在內(nèi)存釋放給堆之前,這塊內(nèi)存不再被原有的對(duì)象使用,而是放到“釋放列表”中以備重用。由于Allocator類需要保存已經(jīng)釋放的內(nèi)存塊,在使用delete操作符時(shí),我們將“釋放列表”中的下一個(gè)指針指向這個(gè)被delete的對(duì)象內(nèi)存地址。當(dāng)應(yīng)用程序再次使用這塊內(nèi)存時(shí),指針被覆寫為對(duì)象的地址。通過這種方法,就不需要預(yù)先實(shí)例化內(nèi)存空間。

使用釋放對(duì)象的內(nèi)存來將內(nèi)存塊連接在一起意味著對(duì)象的內(nèi)存空間需要足夠容納一個(gè)指針占用內(nèi)存空間的大小。構(gòu)造函數(shù)初始化列表中的代碼保證了最小內(nèi)存塊大小不會(huì)小于指針占用內(nèi)存塊的大小。

類的析構(gòu)函數(shù)通過釋放堆內(nèi)存池或者遍歷“釋放列表”并逐個(gè)釋放內(nèi)存塊來實(shí)現(xiàn)內(nèi)存的釋放。由于Allocator類對(duì)象常被用作是static的,那么Allocator對(duì)象的釋放是在程序結(jié)束時(shí)。對(duì)于大多數(shù)嵌入式設(shè)備,應(yīng)用只在人們拔斷電源時(shí)才會(huì)結(jié)束。因此,對(duì)于這種嵌入式設(shè)備,析構(gòu)函數(shù)的作用就顯無所謂了。

如果使用堆內(nèi)存塊模式,除非所有分配的內(nèi)存被鏈接在“釋放列表”,應(yīng)用結(jié)束時(shí)分配的內(nèi)存塊不能被釋放。因此,所有對(duì)象應(yīng)該在程序結(jié)束時(shí)被“刪除”(指放入“釋放列表”)。這似乎是內(nèi)存泄漏,也帶來了一個(gè)有趣的問題。Allocator應(yīng)該跟蹤正在使用和已經(jīng)釋放的內(nèi)存塊嗎?答案是否定的。以為一旦一塊內(nèi)存通過指針被應(yīng)用所使用,那么應(yīng)用程序有責(zé)任在程序結(jié)束前通過調(diào)用Deallocate()返回該內(nèi)存塊指針給Allocator。這樣的話,我么只需要跟蹤釋放的內(nèi)存塊。

代碼的使用

Allocator易于使用,因此創(chuàng)建宏來自動(dòng)在客戶端類中實(shí)現(xiàn)接口。宏提供一個(gè)靜態(tài)類型的Allocator實(shí)例和兩個(gè)成員函數(shù):操作符new和操作符delete。通過重寫new和delete操作符,Allocator截取并處理所有的客戶端類的內(nèi)存分配行為。

DECLARE_ALLOCATOR宏提供頭文件接口,并且應(yīng)該在類定義時(shí)將其包含在內(nèi),如下面這樣:

#include "Allocator.h"class MyClass{
    DECLARE_ALLOCATOR// remaining class definition};

操作符new函數(shù)調(diào)用Allocator創(chuàng)建類實(shí)例所需要的內(nèi)存空間。內(nèi)存分配后,根據(jù)定義,操作符new調(diào)用該類的構(gòu)造函數(shù)。重寫的new只修改了內(nèi)存的分配任務(wù)。構(gòu)造函數(shù)的調(diào)用由語言保證。刪除對(duì)象時(shí),系統(tǒng)首先調(diào)用析構(gòu)函數(shù),然后調(diào)用執(zhí)行操作符delete函數(shù)。操作符delete使用Deallocate()函數(shù)將內(nèi)存塊加入到“釋放列表”中。

盡管沒有明確聲明,操作符delete是靜態(tài)函數(shù)(靜態(tài)函數(shù)才能調(diào)用靜態(tài)成員)。因此它不能被聲明為virtual。這樣看上去通過基類的指針刪除對(duì)象不能達(dá)到刪除真實(shí)對(duì)象的目的。畢竟,調(diào)用基類指針的靜態(tài)函數(shù)只會(huì)調(diào)用基類的成員函數(shù),而不是其真實(shí)類型的成員函數(shù)。然而,我們知道,調(diào)用操作符delete時(shí)首先調(diào)用析構(gòu)函數(shù)。修飾為virtual的析構(gòu)函數(shù)會(huì)實(shí)際調(diào)用子類的析構(gòu)函數(shù)。類的析構(gòu)函數(shù)執(zhí)行完后,子類的操作符delete函數(shù)被調(diào)用。因此實(shí)際上,由于虛析構(gòu)函數(shù)的調(diào)用,重寫的操作符delete會(huì)在子類中調(diào)用。所以,使用基類指針刪除對(duì)象時(shí),基類對(duì)象的析構(gòu)函數(shù)必須聲明為virtual。否則,將會(huì)不能正確調(diào)用析構(gòu)函數(shù)和操作符delete。

IMPLEMENT_ALLOCATOR宏是接口的源文件實(shí)現(xiàn)部分,并應(yīng)該放置于源文件中。

IMPLEMENT_ALLOCATOR(MyClass, 0, 0)

使用上述宏后,可以如下面一樣創(chuàng)建并銷毀類的實(shí)例,同事循環(huán)使用釋放的內(nèi)存空間。

MyClass* myClass = new MyClass();delete myClass;

Allocator類支持單繼承和多繼承。例如,Derived類繼承Base類,如下代碼是正確的。

Base* base = new Derived;
delete base;

運(yùn)行時(shí)

運(yùn)行時(shí),Allocator初始化時(shí)“釋放列表”中沒有可重用的內(nèi)存塊。因此,***次調(diào)用Allocate()將從內(nèi)存池或者堆中獲取內(nèi)存空間。隨著程序的執(zhí)行,系統(tǒng)不斷使用對(duì)象會(huì)造成分配器的波動(dòng)。并且只有當(dāng)釋放列表無法提供內(nèi)存時(shí),新內(nèi)存才會(huì)被申請(qǐng)和創(chuàng)建。最終,系統(tǒng)使用對(duì)象的實(shí)例會(huì)固定,因此每次內(nèi)存分配將會(huì)使用已經(jīng)存在的內(nèi)存空間二不是再從內(nèi)存池或者堆中申請(qǐng)。

與使用內(nèi)存管理器分配所有對(duì)象內(nèi)存相比,Allocator分配器更加高效。內(nèi)存分配時(shí),內(nèi)存指針僅僅是從“釋放列表”中彈出,速度非常快。內(nèi)存釋放時(shí)也僅僅是將內(nèi)存指針放入到“釋放列表”當(dāng)中,速度也十分快。

基準(zhǔn)測試

在Windows PC上使用Allocator和全局堆內(nèi)存的對(duì)比性能測試顯示出Allocator的高性能。測試分配和釋放20000個(gè)4096和2048大小的內(nèi)存塊來測試分配和釋放內(nèi)存的速度。測試的算法詳見附件中的代碼。

AllocatorModeRunBenchmark Time (mS)
Global HeapDebug Heap11640
Global HeapDebug Heap21864
Global HeapDebug Heap31855
Global HeapRelease Heap155
Global HeapRelease Heap247
Global HeapRelease Heap347
AllocatorStatic Pool119
AllocatorStatic Pool27
AllocatorStatic Pool37
AllocatorHeap Blocks130
AllocatorHeap Blocks27
AllocatorHeap Blocks37

使用調(diào)試模式執(zhí)行時(shí),Windows使用調(diào)試堆內(nèi)存。調(diào)試堆內(nèi)存添加額外的安全檢查降低了性能。發(fā)布堆內(nèi)存性能更好,因?yàn)椴皇褂冒踩珯z查。通過在Visual Studio工程選項(xiàng)中,設(shè)置【調(diào)試】-【環(huán)境】中_NO_DEBUG_HEAP=1來禁止調(diào)試內(nèi)存模式。

全局調(diào)試堆內(nèi)存模式需要平均1.8秒,是最慢的。釋放對(duì)內(nèi)存模式50毫秒左右,稍快。基準(zhǔn)測試的場景非常簡單,實(shí)際情況下,不同大小的內(nèi)存塊和隨機(jī)的申請(qǐng)、釋放可能產(chǎn)生不同的結(jié)果。然而,最簡單的也最能說明問題。內(nèi)存管理器比Allocator內(nèi)存分配器慢,并且很大程度上依賴于平臺(tái)的實(shí)現(xiàn)能力。

內(nèi)存分配器Allocator使用靜態(tài)內(nèi)存模式不依賴于堆內(nèi)存的分配。一旦“釋放列表”中含有內(nèi)存塊后,其執(zhí)行時(shí)間大約為7毫秒。***次耗時(shí)19毫秒用于將內(nèi)存池中的內(nèi)存防止到Allocator分配器中管理。

Aloocator使用堆內(nèi)存模式時(shí),當(dāng)“釋放列表”中有可重用的內(nèi)存后,其速度與靜態(tài)內(nèi)存模式一樣快。堆內(nèi)存模式依賴于全局堆來獲取內(nèi)存塊,但是循環(huán)利用“釋放列表”中的內(nèi)存。***次需要申請(qǐng)堆內(nèi)存,耗時(shí)30毫秒。由于重用“釋放列表”中的內(nèi)存,之后的申請(qǐng)僅需要7毫秒。

上面的基準(zhǔn)測試結(jié)果表示,Allocator內(nèi)存分配器更加高效,擁有7倍于Windows全局發(fā)布堆內(nèi)存模式的速度。

對(duì)于嵌入式系統(tǒng),我使用Keil在ARM STM32F4 CPU(168Hz)上運(yùn)行相同測試。由于資源限制,我將***內(nèi)存塊數(shù)量降低到500,單個(gè)內(nèi)存塊大小降低到32和16字節(jié)。下面是結(jié)果:

AllocatorModeRunBenchmark Time (mS)
Global HeapRelease111.6
Global HeapRelease211.6
Global HeapRelease311.6
AllocatorStatic Pool10.85
AllocatorStatic Pool20.79
AllocatorStatic Pool30.79
AllocatorHeap Blocks11.19
AllocatorHeap Blocks20.79
AllocatorHeap Blocks30.79

基于ARM的基準(zhǔn)測試顯示,使用Allocator分配器的類性能快15倍。這個(gè)結(jié)果會(huì)讓Keil堆內(nèi)存的表現(xiàn)相形見絀。基準(zhǔn)測試分配500個(gè)16字節(jié)大小的內(nèi)存塊進(jìn)行測試。每個(gè)16字節(jié)大小的內(nèi)存刪除后申請(qǐng)500個(gè)32字節(jié)大小的內(nèi)存塊。全局堆內(nèi)存耗時(shí)11.6毫秒,而且,在內(nèi)存碎片化后,內(nèi)存管理器可能會(huì)在沒有安全檢查的情況下耗時(shí)更大。

分配器決議

***個(gè)決定是你是否需要使用分配器。如果你的項(xiàng)目不關(guān)心執(zhí)行的速度和是否需要容錯(cuò),那么你可能不需要自定義的分配器,全局堆分配管理器足夠用了。

另一方面,如果你需要考慮執(zhí)行速度和容錯(cuò)管理,分配器會(huì)起到作用。你需要根據(jù)項(xiàng)目的需要選擇分配器的模式。重要任務(wù)系統(tǒng)的設(shè)計(jì)可能強(qiáng)制要求使用全局堆內(nèi)存。而動(dòng)態(tài)分配內(nèi)存可能更高效,設(shè)計(jì)更優(yōu)雅。這種情況下,你可以在調(diào)試開發(fā)時(shí)使用堆內(nèi)存模式獲取內(nèi)存使用參數(shù),然后發(fā)布時(shí)切換到靜態(tài)內(nèi)存池模式避免內(nèi)存分配帶來的性能消耗。一些編譯時(shí)的宏可用于模式的切換。

另外,堆內(nèi)存模式可能對(duì)應(yīng)用更適合。該模式利用堆來獲取新內(nèi)存,同時(shí)阻止了堆碎片錯(cuò)誤。當(dāng)“釋放列表”鏈接足夠的內(nèi)存塊后更能加快內(nèi)存的分配效率。

在源代碼中沒有實(shí)現(xiàn)的涉及多線程的問題不在本文的討論范圍內(nèi)。運(yùn)行系統(tǒng)一會(huì)后,可以方便地使用GetlockCount函數(shù)和GetName函數(shù)獲取內(nèi)存塊數(shù)量和名稱。這些度量參數(shù)提供關(guān)于內(nèi)存分配的信息。盡量多申請(qǐng)點(diǎn)內(nèi)存,以便給分配盤一些彈性來避免內(nèi)存耗盡。

調(diào)試內(nèi)存泄漏

調(diào)試內(nèi)存泄漏非常困難,原因是堆內(nèi)存就像一個(gè)黑盒,對(duì)于分配對(duì)象的類型和大小是不可見的。使用Allocator,由于Allocator跟蹤記錄內(nèi)存塊的總數(shù),內(nèi)存泄漏檢查變得簡單一點(diǎn)。對(duì)每個(gè)分配器實(shí)例重復(fù)輸出(例如輸出到終端)GetBlockCount和GetName并比對(duì)它們的不同能讓我們更好的了解分配器對(duì)內(nèi)存的分配。

錯(cuò)誤處理

C++中使用new_handler函數(shù)處理內(nèi)存分配錯(cuò)誤。如果內(nèi)存管理器在申請(qǐng)內(nèi)存時(shí)發(fā)生錯(cuò)誤,用戶的錯(cuò)誤處理函數(shù)就會(huì)被調(diào)用。通過將用戶的錯(cuò)誤處理函數(shù)地址復(fù)制給new_handler,內(nèi)存管理器就能調(diào)用用戶自定義的錯(cuò)誤處理程序。為了讓Allocator類的錯(cuò)誤處理機(jī)制與內(nèi)存管理器保持一致,分配器也通過new_handler調(diào)用錯(cuò)誤處理函數(shù),集中處理所有的內(nèi)存分配錯(cuò)誤。

static void out_of_memory() {// new-handler function called by Allocator when pool is out of memoryassert(0);
}int _tmain(int argc, _TCHAR* argv[]) {
    std::set_new_handler(out_of_memory);
...

限制

分配器類不支持?jǐn)?shù)組對(duì)象的內(nèi)存分配。為每一個(gè)對(duì)象創(chuàng)建分開的內(nèi)存是無法保證的,因?yàn)閚ew的多次調(diào)用不保證內(nèi)存塊的連續(xù),但這又是數(shù)組所需要的。因此Allocator只支持固定大小內(nèi)存塊的分配,對(duì)象數(shù)組不支持。

移植問題

Allocator在靜態(tài)內(nèi)存池耗盡時(shí)調(diào)用new_handle指向的函數(shù),這對(duì)于某些系統(tǒng)不合適。假設(shè)new_handle函數(shù)沒返回,例如無盡的循環(huán)或者斷言,調(diào)用這個(gè)函數(shù)不起任何作用。使用固定內(nèi)存池時(shí)這無濟(jì)于事。

感謝各位的閱讀,以上就是“C++固定內(nèi)存塊分配器相關(guān)知識(shí)點(diǎn)有哪些”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)C++固定內(nèi)存塊分配器相關(guān)知識(shí)點(diǎn)有哪些這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI