使用說明1:
調(diào)用方法

使用時注意幾點(diǎn):
1. ThreadJob 沒什么用,直接寫線程函數(shù)吧。
2. 線程函數(shù)(threadfunc)的入口參數(shù)void* 可以轉(zhuǎn)成自定義的類型對象,這個對象可以記錄下線程運(yùn)行中的數(shù)據(jù),并設(shè)置線程當(dāng)前狀態(tài),以此與線程進(jìn)行交互。
3. 線程池有一個EndAndWait函數(shù),用于讓線程池中所有計(jì)算正常結(jié)束。有時線程池中的一個線程可能要運(yùn)行很長時間,怎么辦?可以通過線程函數(shù)threadfunc的入口參數(shù)對象來處理,比如:
在主線程中設(shè)置yourClass->cmd = 1,該線程就會自然結(jié)束。
使用說明2:

.Critical section(臨界區(qū))用來實(shí)現(xiàn)“排他性占有”。適用范圍是單一進(jìn)程的各線程之間。它是:
· 一個局部性對象,不是一個核心對象。
· 快速而有效率。
· 不能夠同時有一個以上的critical section被等待。
· 無法偵測是否已被某個線程放棄。
Mutex
Mutex是一個核心對象,可以在不同的線程之間實(shí)現(xiàn)“排他性占有”,甚至幾十那些現(xiàn)成分屬不同進(jìn)程。它是:
· 一個核心對象。
· 如果擁有mutex的那個線程結(jié)束,則會產(chǎn)生一個“abandoned”錯誤信息。
· 可以使用Wait…()等待一個mutex。
· 可以具名,因此可以被其他進(jìn)程開啟。
· 只能被擁有它的那個線程釋放(released)。
Semaphore
Semaphore被用來追蹤有限的資源。它是:
· 一個核心對象。
· 沒有擁有者。
· 可以具名,因此可以被其他進(jìn)程開啟。
· 可以被任何一個線程釋放(released)。
Event Object
Event object通常使用于overlapped I/O,或用來設(shè)計(jì)某些自定義的同步對象。它是:
· 一個核心對象。
· 完全在程序掌控之下。
· 適用于設(shè)計(jì)新的同步對象。
· “要求蘇醒”的請求并不會被儲存起來,可能會遺失掉。
· 可以具名,因此可以被其他進(jìn)程開啟。
Interlocked Variable
如果Interlocked…()函數(shù)被使用于所謂的spin-lock,那么他們只是一種同步機(jī)制。所謂spin-lock是一種busy loop,被預(yù)期在極短時間內(nèi)執(zhí)行,所以有最小的額外負(fù)擔(dān)(overhead)。系統(tǒng)核心偶爾會使用他們。除此之外,interlocked variables主要用于引用技術(shù)。他們:
· 允許對4字節(jié)的數(shù)值有些基本的同步操作,不需動用到critical section或mutex之類。
· 在SMP(Symmetric Multi-Processors)操作系統(tǒng)中亦可有效運(yùn)作。
"placement new"?
它
到底是什么東東呀?我也是最近幾天才聽說,看來對于C++我還差很遠(yuǎn)呀!placement new 是重載operator
new的一個標(biāo)準(zhǔn)、全局的版本,它不能被自定義的版本代替(不像普通的operator new和operator
delete能夠被替換成用戶自定義的版本)。
它的原型如下:
void *operator new( size_t, void *p ) throw() { return p; }
首先我們區(qū)分下幾個容易混淆的關(guān)鍵詞:new、operator new、placement new
new和delete操作符我們應(yīng)該都用過,它們是對堆中的內(nèi)存進(jìn)行申請和釋放,而這兩個都是不能被重載的。要實(shí)現(xiàn)不同的內(nèi)存分配行為,需要重載operator new,而不是new和delete。![]()
看如下代碼:
class MyClass {…};
MyClass * p=new MyClass;
這里的new實(shí)際上是執(zhí)行如下3個過程:
1. 調(diào)用operator new分配內(nèi)存 ;2. 調(diào)用構(gòu)造函數(shù)生成類對象;3. 返回相應(yīng)指針。
operator new就像operator+一樣,是可以重載的,但是不能在全局對原型為void operator new(size_t size)這個原型進(jìn)行重載,一般只能在類中進(jìn)行重載。如果類中沒有重載operator new,那么調(diào)用的就是全局的::operator new來完成堆的分配。同理,operator new[]、operator delete、operator delete[]也是可以重載的,一般你重載的其中一個,那么最后把其余的三個都重載一遍。
至于placement new才是本文的重點(diǎn)。其實(shí)它也只是operator new的一個重載的版本,只是我們很少用到它。如果你想在已經(jīng)分配的內(nèi)存中創(chuàng)建一個對象,使用new時行不通的。也就是說placement new允許你在一個已經(jīng)分配好的內(nèi)存中(棧或者堆中)構(gòu)造一個新的對象。原型中void*p實(shí)際上就是指向一個已經(jīng)分配好的內(nèi)存緩沖區(qū)的的首地址。
我們知道使用new操作符分配內(nèi)存需要在堆中查找足夠大的剩余空間,這個操作速度是很慢的,而且有可能出現(xiàn)無法分配內(nèi)存的異常(空間不夠)。 placement new就可以解決這個問題。我們構(gòu)造對象都是在一個預(yù)先準(zhǔn)備好了的內(nèi)存緩沖區(qū)中進(jìn)行,不需要查找內(nèi)存,內(nèi)存分配的時間是常數(shù);而且不會出現(xiàn)在程序運(yùn)行中途 出現(xiàn)內(nèi)存不足的異常。所以,placement new非常適合那些對時間要求比較高,長時間運(yùn)行不希望被打斷的應(yīng)用程序。
使用方法如下:
1. 緩沖區(qū)提前分配
可以使用堆的空間,也可以使用棧的空間,所以分配方式有如下兩種:
class MyClass {…};
char *buf=new char[N*sizeof(MyClass)+ sizeof(int) ] ; 或者char buf[N*sizeof(MyClass)+ sizeof(int) ];
2. 對象的構(gòu)造
MyClass * pClass=new(buf) MyClass;
3. 對象的銷毀
一旦這個對象使用完畢,你必須顯式的調(diào)用類的析構(gòu)函數(shù)進(jìn)行銷毀對象。但此時內(nèi)存空間不會被釋放,以便其他的對象的構(gòu)造。
pClass->~MyClass();
4. 內(nèi)存的釋放
如果緩沖區(qū)在堆中,那么調(diào)用delete[] buf;進(jìn)行內(nèi)存的釋放;如果在棧中,那么在其作用域內(nèi)有效,跳出作用域,內(nèi)存自動釋放。
注意:
本章首先簡單介紹自定義內(nèi)存池性能優(yōu)化的原理,然后列舉軟件開發(fā)中常用的內(nèi)存池的不同類型,并給出具體實(shí)現(xiàn)的實(shí)例。
本 書主要針對的是 C++ 程序的性能優(yōu)化,深入介紹 C++ 程序性能優(yōu)化的方法和實(shí)例。全書由 4 個篇組成,第 1 篇介紹 C++ 語言的對象模型,該篇是優(yōu)化 C++ 程序的基礎(chǔ);第 2 篇主要針對如何優(yōu)化 C++ 程序的內(nèi)存使用;第 3 篇介紹如何優(yōu)化程序的啟動性能;第 4 篇介紹了三類性能優(yōu)化工具,即內(nèi)存分析工具、性能分析工具和 I/O 檢測工具,它們是測量程序性能的利器。
在此我們推出了此書的第 2、6 章供大家在線瀏覽。更多推薦書籍請?jiān)L問 developerWorks 圖書頻道。
|
|
||||||||
如 前所述,讀者已經(jīng)了解到"堆"和"棧"的區(qū)別。而在編程實(shí)踐中,不可避免地要大量用到堆上的內(nèi)存。例如在程序中維護(hù)一個鏈表的數(shù)據(jù)結(jié)構(gòu)時,每次新增或者刪 除一個鏈表的節(jié)點(diǎn),都需要從內(nèi)存堆上分配或者釋放一定的內(nèi)存;在維護(hù)一個動態(tài)數(shù)組時,如果動態(tài)數(shù)組的大小不能滿足程序需要時,也要在內(nèi)存堆上分配新的內(nèi)存 空間。
6.1.1 默認(rèn)內(nèi)存管理函數(shù)的不足
利用默認(rèn)的內(nèi)存管理函數(shù)new/delete或malloc/free在堆上分配和釋放內(nèi)存會有一些額外的開銷。
系 統(tǒng)在接收到分配一定大小內(nèi)存的請求時,首先查找內(nèi)部維護(hù)的內(nèi)存空閑塊表,并且需要根據(jù)一定的算法(例如分配最先找到的不小于申請大小的內(nèi)存塊給請求者,或 者分配最適于申請大小的內(nèi)存塊,或者分配最大空閑的內(nèi)存塊等)找到合適大小的空閑內(nèi)存塊。如果該空閑內(nèi)存塊過大,還需要切割成已分配的部分和較小的空閑 塊。然后系統(tǒng)更新內(nèi)存空閑塊表,完成一次內(nèi)存分配。類似地,在釋放內(nèi)存時,系統(tǒng)把釋放的內(nèi)存塊重新加入到空閑內(nèi)存塊表中。如果有可能的話,可以把相鄰的空 閑塊合并成較大的空閑塊。
默認(rèn)的內(nèi)存管理函數(shù)還考慮到多線程的應(yīng)用,需要在每次分配和釋放內(nèi)存時加鎖,同樣增加了開銷。
可見,如果應(yīng)用程序頻繁地在堆上分配和釋放內(nèi)存,則會導(dǎo)致性能的損失。并且會使系統(tǒng)中出現(xiàn)大量的內(nèi)存碎片,降低內(nèi)存的利用率。
默認(rèn)的分配和釋放內(nèi)存算法自然也考慮了性能,然而這些內(nèi)存管理算法的通用版本為了應(yīng)付更復(fù)雜、更廣泛的情況,需要做更多的額外工作。而對于某一個具體的應(yīng)用程序來說,適合自身特定的內(nèi)存分配釋放模式的自定義內(nèi)存池則可以獲得更好的性能。
自 定義內(nèi)存池的思想通過這個"池"字表露無疑,應(yīng)用程序可以通過系統(tǒng)的內(nèi)存分配調(diào)用預(yù)先一次性申請適當(dāng)大小的內(nèi)存作為一個內(nèi)存池,之后應(yīng)用程序自己對內(nèi)存的 分配和釋放則可以通過這個內(nèi)存池來完成。只有當(dāng)內(nèi)存池大小需要動態(tài)擴(kuò)展時,才需要再調(diào)用系統(tǒng)的內(nèi)存分配函數(shù),其他時間對內(nèi)存的一切操作都在應(yīng)用程序的掌控 之中。
應(yīng)用程序自定義的內(nèi)存池根據(jù)不同的適用場景又有不同的類型。
從 線程安全的角度來分,內(nèi)存池可以分為單線程內(nèi)存池和多線程內(nèi)存池。單線程內(nèi)存池整個生命周期只被一個線程使用,因而不需要考慮互斥訪問的問題;多線程內(nèi)存 池有可能被多個線程共享,因此則需要在每次分配和釋放內(nèi)存時加鎖。相對而言,單線程內(nèi)存池性能更高,而多線程內(nèi)存池適用范圍更廣。
從內(nèi)存池可分配內(nèi)存單元大小來分,可以分為固定內(nèi)存池和可變內(nèi)存池。所謂固定內(nèi)存池是指應(yīng)用程序每次從內(nèi)存池中分配出來的內(nèi)存單元大小事先已經(jīng)確定,是固定不變的;而可變內(nèi)存池則每次分配的內(nèi)存單元大小可以按需變化,應(yīng)用范圍更廣,而性能比固定內(nèi)存池要低。
下面以固定內(nèi)存池為例說明內(nèi)存池的工作原理,如圖6-1所示。
固定內(nèi)存池由一系列固定大小的內(nèi)存塊組成,每一個內(nèi)存塊又包含了固定數(shù)量和大小的內(nèi)存單元。
如 圖6-1所示,該內(nèi)存池一共包含4個內(nèi)存塊。在內(nèi)存池初次生成時,只向系統(tǒng)申請了一個內(nèi)存塊,返回的指針作為整個內(nèi)存池的頭指針。之后隨著應(yīng)用程序?qū)?nèi)存 的不斷需求,內(nèi)存池判斷需要動態(tài)擴(kuò)大時,才再次向系統(tǒng)申請新的內(nèi)存塊,并把所有這些內(nèi)存塊通過指針鏈接起來。對于操作系統(tǒng)來說,它已經(jīng)為該應(yīng)用程序分配了 4個等大小的內(nèi)存塊。由于是大小固定的,所以分配的速度比較快;而對于應(yīng)用程序來說,其內(nèi)存池開辟了一定大小,內(nèi)存池內(nèi)部卻還有剩余的空間。
例 如放大來看第4個內(nèi)存塊,其中包含一部分內(nèi)存池塊頭信息和3個大小相等的內(nèi)存池單元。單元1和單元3是空閑的,單元2已經(jīng)分配。當(dāng)應(yīng)用程序需要通過該內(nèi)存 池分配一個單元大小的內(nèi)存時,只需要簡單遍歷所有的內(nèi)存池塊頭信息,快速定位到還有空閑單元的那個內(nèi)存池塊。然后根據(jù)該塊的塊頭信息直接定位到第1個空閑 的單元地址,把這個地址返回,并且標(biāo)記下一個空閑單元即可;當(dāng)應(yīng)用程序釋放某一個內(nèi)存池單元時,直接在對應(yīng)的內(nèi)存池塊頭信息中標(biāo)記該內(nèi)存單元為空閑單元即 可。
可見與系統(tǒng)管理內(nèi)存相比,內(nèi)存池的操作非常迅速,它在性能優(yōu)化方面的優(yōu)點(diǎn)主要如下。
(1)針對特殊情況,例如需要頻繁分配釋放固定大小的內(nèi)存對象時,不需要復(fù)雜的分配算法和多線程保護(hù)。也不需要維護(hù)內(nèi)存空閑表的額外開銷,從而獲得較高的性能。
(2)由于開辟一定數(shù)量的連續(xù)內(nèi)存空間作為內(nèi)存池塊,因而一定程度上提高了程序局部性,提升了程序性能。
(3)比較容易控制頁邊界對齊和內(nèi)存字節(jié)對齊,沒有內(nèi)存碎片的問題。
|
6.2 一個內(nèi)存池的實(shí)現(xiàn)實(shí)例
本節(jié)分析在某個大型應(yīng)用程序?qū)嶋H應(yīng)用到的一個內(nèi)存池實(shí)現(xiàn),并詳細(xì)講解其使用方法與工作原理。這是一個應(yīng)用于單線程環(huán)境且分配單元大小固定的內(nèi)存池,一般用來為執(zhí)行時會動態(tài)頻繁地創(chuàng)建且可能會被多次創(chuàng)建的類對象或者結(jié)構(gòu)體分配內(nèi)存。
本節(jié)首先講解該內(nèi)存池的數(shù)據(jù)結(jié)構(gòu)聲明及圖示,接著描述其原理及行為特征。然后逐一講解實(shí)現(xiàn)細(xì)節(jié),最后介紹如何在實(shí)際程序中應(yīng)用此內(nèi)存池,并與使用普通內(nèi)存函數(shù)申請內(nèi)存的程序性能作比較。
內(nèi)存池類MemoryPool的聲明如下:
class MemoryPool |
MemoryBlock為內(nèi)存池中附著在真正用來為內(nèi)存請求分配內(nèi)存的內(nèi)存塊頭部的結(jié)構(gòu)體,它描述了與之聯(lián)系的內(nèi)存塊的使用信息:
struct MemoryBlock |
此內(nèi)存池的數(shù)據(jù)結(jié)構(gòu)如圖6-2所示。
此內(nèi)存池的總體機(jī)制如下。
(1) 在運(yùn)行過程中,MemoryPool內(nèi)存池可能會有多個用來滿足內(nèi)存申請請求的內(nèi)存塊,這些內(nèi)存塊是從進(jìn)程堆中開辟的一個較大的連續(xù)內(nèi)存區(qū)域,它由一個 MemoryBlock結(jié)構(gòu)體和多個可供分配的內(nèi)存單元組成,所有內(nèi)存塊組成了一個內(nèi)存塊鏈表,MemoryPool的pBlock是這個鏈表的頭。對每 個內(nèi)存塊,都可以通過其頭部的MemoryBlock結(jié)構(gòu)體的pNext成員訪問緊跟在其后面的那個內(nèi)存塊。
(2) 每個內(nèi)存塊由兩部分組成,即一個MemoryBlock結(jié)構(gòu)體和多個內(nèi)存分配單元。這些內(nèi)存分配單元大小固定(由MemoryPool的 nUnitSize表示),MemoryBlock結(jié)構(gòu)體并不維護(hù)那些已經(jīng)分配的單元的信息;相反,它只維護(hù)沒有分配的自由分配單元的信息。它有兩個成員 比較重要:nFree和nFirst。nFree記錄這個內(nèi)存塊中還有多少個自由分配單元,而nFirst則記錄下一個可供分配的單元的編號。每一個自由 分配單元的頭兩個字節(jié)(即一個USHORT型值)記錄了緊跟它之后的下一個自由分配單元的編號,這樣,通過利用每個自由分配單元的頭兩個字節(jié),一個 MemoryBlock中的所有自由分配單元被鏈接起來。
(3)當(dāng)有新的內(nèi)存請求到來 時,MemoryPool會通過pBlock遍歷MemoryBlock鏈表,直到找到某個MemoryBlock所在的內(nèi)存塊,其中還有自由分配單元 (通過檢測MemoryBlock結(jié)構(gòu)體的nFree成員是否大于0)。如果找到這樣的內(nèi)存塊,取得其MemoryBlock的nFirst值(此為該內(nèi) 存塊中第1個可供分配的自由單元的編號)。然后根據(jù)這個編號定位到該自由分配單元的起始位置(因?yàn)樗蟹峙鋯卧笮」潭ǎ虼嗣總€分配單元的起始位置都可 以通過編號分配單元大小來偏移定位),這個位置就是用來滿足此次內(nèi)存申請請求的內(nèi)存的起始地址。但在返回這個地址前,需要首先將該位置開始的頭兩個字節(jié)的 值(這兩個字節(jié)值記錄其之后的下一個自由分配單元的編號)賦給本內(nèi)存塊的MemoryBlock的nFirst成員。這樣下一次的請求就會用這個編號對應(yīng) 的內(nèi)存單元來滿足,同時將此內(nèi)存塊的MemoryBlock的nFree遞減1,然后才將剛才定位到的內(nèi)存單元的起始位置作為此次內(nèi)存請求的返回地址返回 給調(diào)用者。
(4)如果從現(xiàn)有的內(nèi)存塊中找不到一個自由的內(nèi)存分配單元(當(dāng)?shù)?次請求內(nèi)存,以及現(xiàn)有的所有內(nèi)存 塊中的所有內(nèi)存分配單元都已經(jīng)被分配時會發(fā)生這種情形),MemoryPool就會從進(jìn)程堆中申請一個內(nèi)存塊(這個內(nèi)存塊包括一個MemoryBlock 結(jié)構(gòu)體,及緊鄰其后的多個內(nèi)存分配單元,假設(shè)內(nèi)存分配單元的個數(shù)為n,n可以取值MemoryPool中的nInitSize或者nGrowSize), 申請完后,并不會立刻將其中的一個分配單元分配出去,而是需要首先初始化這個內(nèi)存塊。初始化的操作包括設(shè)置MemoryBlock的nSize為所有內(nèi)存 分配單元的大小(注意,并不包括MemoryBlock結(jié)構(gòu)體的大小)、nFree為n-1(注意,這里是n-1而不是n,因?yàn)榇舜涡聝?nèi)存塊就是為了滿足 一次新的內(nèi)存請求而申請的,馬上就會分配一塊自由存儲單元出去,如果設(shè)為n-1,分配一個自由存儲單元后無須再將n遞減1),nFirst為1(已經(jīng)知道 nFirst為下一個可以分配的自由存儲單元的編號。為1的原因與nFree為n-1相同,即立即會將編號為0的自由分配單元分配出去。現(xiàn)在設(shè)為1,其后 不用修改nFirst的值),MemoryBlock的構(gòu)造需要做更重要的事情,即將編號為0的分配單元之后的所有自由分配單元鏈接起來。如前所述,每個 自由分配單元的頭兩個字節(jié)用來存儲下一個自由分配單元的編號。另外,因?yàn)槊總€分配單元大小固定,所以可以通過其編號和單元大小(MemoryPool的 nUnitSize成員)的乘積作為偏移值進(jìn)行定位。現(xiàn)在唯一的問題是定位從哪個地址開始?答案是MemoryBlock的aData[1]成員開始。因 為aData[1]實(shí)際上是屬于MemoryBlock結(jié)構(gòu)體的(MemoryBlock結(jié)構(gòu)體的最后一個字節(jié)),所以實(shí)質(zhì)上,MemoryBlock結(jié) 構(gòu)體的最后一個字節(jié)也用做被分配出去的分配單元的一部分。因?yàn)檎麄€內(nèi)存塊由MemoryBlock結(jié)構(gòu)體和整數(shù)個分配單元組成,這意味著內(nèi)存塊的最后一個 字節(jié)會被浪費(fèi),這個字節(jié)在圖6-2中用位于兩個內(nèi)存的最后部分的濃黑背景的小塊標(biāo)識。確定了分配單元的起始位置后,將自由分配單元鏈接起來的工作就很容易 了。即從aData位置開始,每隔nUnitSize大小取其頭兩個字節(jié),記錄其之后的自由分配單元的編號。因?yàn)閯傞_始所有分配單元都是自由的,所以這個 編號就是自身編號加1,即位置上緊跟其后的單元的編號。初始化后,將此內(nèi)存塊的第1個分配單元的起始地址返回,已經(jīng)知道這個地址就是aData。
(5) 當(dāng)某個被分配的單元因?yàn)閐elete需要回收時,該單元并不會返回給進(jìn)程堆,而是返回給MemoryPool。返回時,MemoryPool能夠知道該單 元的起始地址。這時,MemoryPool開始遍歷其所維護(hù)的內(nèi)存塊鏈表,判斷該單元的起始地址是否落在某個內(nèi)存塊的地址范圍內(nèi)。如果不在所有內(nèi)存地址范 圍內(nèi),則這個被回收的單元不屬于這個MemoryPool;如果在某個內(nèi)存塊的地址范圍內(nèi),那么它會將這個剛剛回收的分配單元加到這個內(nèi)存塊的 MemoryBlock所維護(hù)的自由分配單元鏈表的頭部,同時將其nFree值遞增1。回收后,考慮到資源的有效利用及后續(xù)操作的性能,內(nèi)存池的操作會繼 續(xù)判斷:如果此內(nèi)存塊的所有分配單元都是自由的,那么這個內(nèi)存塊就會從MemoryPool中被移出并作為一個整體返回給進(jìn)程堆;如果該內(nèi)存塊中還有非自 由分配單元,這時不能將此內(nèi)存塊返回給進(jìn)程堆。但是因?yàn)閯倓傆幸粋€分配單元返回給了這個內(nèi)存塊,即這個內(nèi)存塊有自由分配單元可供下次分配,因此它會被移到 MemoryPool維護(hù)的內(nèi)存塊的頭部。這樣下次的內(nèi)存請求到來,MemoryPool遍歷其內(nèi)存塊鏈表以尋找自由分配單元時,第1次尋找就會找到這個 內(nèi)存塊。因?yàn)檫@個內(nèi)存塊確實(shí)有自由分配單元,這樣可以減少M(fèi)emoryPool的遍歷次數(shù)。
綜上所述,每個內(nèi) 存池(MemoryPool)維護(hù)一個內(nèi)存塊鏈表(單鏈表),每個內(nèi)存塊由一個維護(hù)該內(nèi)存塊信息的塊頭結(jié)構(gòu)(MemoryBlock)和多個分配單元組 成,塊頭結(jié)構(gòu)MemoryBlock則進(jìn)一步維護(hù)一個該內(nèi)存塊的所有自由分配單元組成的"鏈表"。這個鏈表不是通過"指向下一個自由分配單元的指針"鏈接 起來的,而是通過"下一個自由分配單元的編號"鏈接起來,這個編號值存儲在該自由分配單元的頭兩個字節(jié)中。另外,第1個自由分配單元的起始位置并不是 MemoryBlock結(jié)構(gòu)體"后面的"第1個地址位置,而是MemoryBlock結(jié)構(gòu)體"內(nèi)部"的最后一個字節(jié)aData(也可能不是最后一個,因?yàn)? 考慮到字節(jié)對齊的問題),即分配單元實(shí)際上往前面錯了一位。又因?yàn)镸emoryBlock結(jié)構(gòu)體后面的空間剛好是分配單元的整數(shù)倍,這樣依次錯位下去,內(nèi) 存塊的最后一個字節(jié)實(shí)際沒有被利用。這么做的一個原因也是考慮到不同平臺的移植問題,因?yàn)椴煌脚_的對齊方式可能不盡相同。即當(dāng)申請 MemoryBlock大小內(nèi)存時,可能會返回比其所有成員大小總和還要大一些的內(nèi)存。最后的幾個字節(jié)是為了"補(bǔ)齊",而使得aData成為第1個分配單 元的起始位置,這樣在對齊方式不同的各種平臺上都可以工作。
有了上述的總體印象后,本節(jié)來仔細(xì)剖析其實(shí)現(xiàn)細(xì)節(jié)。
(1)MemoryPool的構(gòu)造如下:
MemoryPool::MemoryPool( USHORT _nUnitSize, |
從①處可以看出,MemoryPool創(chuàng)建時,并沒有立刻創(chuàng)建真正用來滿足內(nèi)存申請的內(nèi)存塊,即內(nèi)存塊鏈表剛開始時為空。
②處和③處分別設(shè)置"第1次創(chuàng)建的內(nèi)存塊所包含的分配單元的個數(shù)",及"隨后創(chuàng)建的內(nèi)存塊所包含的分配單元的個數(shù)",這兩個值在MemoryPool創(chuàng)建時通過參數(shù)指定,其后在該MemoryPool對象生命周期中一直不變。
后 面的代碼用來設(shè)置nUnitSize,這個值參考傳入的_nUnitSize參數(shù)。但是還需要考慮兩個因素。如前所述,每個分配單元在自由狀態(tài)時,其頭兩 個字節(jié)用來存放"其下一個自由分配單元的編號"。即每個分配單元"最少"有"兩個字節(jié)",這就是⑤處賦值的原因。④處是將大于4個字節(jié)的大小 _nUnitSize往上"取整到"大于_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍數(shù)(前提是MEMPOOL_ALIGNMENT為2的倍數(shù))。如_nUnitSize為11 時,MEMPOOL_ALIGNMENT為8,nUnitSize為16;MEMPOOL_ALIGNMENT為4,nUnitSize為 12;MEMPOOL_ALIGNMENT為2,nUnitSize為12,依次類推。
(2)當(dāng)向MemoryPool提出內(nèi)存請求時:
void* MemoryPool::Alloc() |
MemoryPool滿足內(nèi)存請求的步驟主要由四步組成。
① 處首先判斷內(nèi)存池當(dāng)前內(nèi)存塊鏈表是否為空,如果為空,則意味著這是第1次內(nèi)存申請請求。這時,從進(jìn)程堆中申請一個分配單元個數(shù)為nInitSize的內(nèi)存 塊,并初始化該內(nèi)存塊(主要初始化MemoryBlock結(jié)構(gòu)體成員,以及創(chuàng)建初始的自由分配單元鏈表,下面會詳細(xì)分析其代碼)。如果該內(nèi)存塊申請成功, 并初始化完畢,返回第1個分配單元給調(diào)用函數(shù)。第1個分配單元以MemoryBlock結(jié)構(gòu)體內(nèi)的最后一個字節(jié)為起始地址。
②處的作用是當(dāng)內(nèi)存池中已有內(nèi)存塊(即內(nèi)存塊鏈表不為空)時遍歷該內(nèi)存塊鏈表,尋找還有"自由分配單元"的內(nèi)存塊。
③ 處檢查如果找到還有自由分配單元的內(nèi)存塊,則"定位"到該內(nèi)存塊現(xiàn)在可以用的自由分配單元處。"定位"以MemoryBlock結(jié)構(gòu)體內(nèi)的最后一個字節(jié)位 置aData為起始位置,以MemoryPool的nUnitSize為步長來進(jìn)行。找到后,需要修改MemoryBlock的nFree信息(剩下來的 自由分配單元比原來減少了一個),以及修改此內(nèi)存塊的自由存儲單元鏈表的信息。在找到的內(nèi)存塊中,pMyBlock->nFirst為該內(nèi)存塊中自 由存儲單元鏈表的表頭,其下一個自由存儲單元的編號存放在pMyBlock->nFirst指示的自由存儲單元(亦即剛才定位到的自由存儲單元)的 頭兩個字節(jié)。通過剛才定位到的位置,取其頭兩個字節(jié)的值,賦給pMyBlock->nFirst,這就是此內(nèi)存塊的自由存儲單元鏈表的新的表頭,即 下一次分配出去的自由分配單元的編號(如果nFree大于零的話)。修改維護(hù)信息后,就可以將剛才定位到的自由分配單元的地址返回給此次申請的調(diào)用函數(shù)。 注意,因?yàn)檫@個分配單元已經(jīng)被分配,而內(nèi)存塊無須維護(hù)已分配的分配單元,因此該分配單元的頭兩個字節(jié)的信息已經(jīng)沒有用處。換個角度看,這個自由分配單元返 回給調(diào)用函數(shù)后,調(diào)用函數(shù)如何處置這塊內(nèi)存,內(nèi)存池?zé)o從知曉,也無須知曉。此分配單元在返回給調(diào)用函數(shù)時,其內(nèi)容對于調(diào)用函數(shù)來說是無意義的。因此幾乎可 以肯定調(diào)用函數(shù)在用這個單元的內(nèi)存時會覆蓋其原來的內(nèi)容,即頭兩個字節(jié)的內(nèi)容也會被抹去。因此每個存儲單元并沒有因?yàn)樾枰溄佣攵嘤嗟木S護(hù)信息,而是 直接利用單元內(nèi)的頭兩個字節(jié),當(dāng)其分配后,頭兩個字節(jié)也可以被調(diào)用函數(shù)利用。而在自由狀態(tài)時,則用來存放維護(hù)信息,即下一個自由分配單元的編號,這是一個 有效利用內(nèi)存的好例子。
④處表示在②處遍歷時,沒有找到還有自由分配單元的內(nèi)存塊,這時,需要重新向進(jìn)程堆申 請一個內(nèi)存塊。因?yàn)椴皇堑谝淮紊暾垉?nèi)存塊,所以申請的內(nèi)存塊包含的分配單元個數(shù)為nGrowSize,而不再是nInitSize。與①處相同,先做這個 新申請內(nèi)存塊的初始化工作,然后將此內(nèi)存塊插入MemoryPool的內(nèi)存塊鏈表的頭部,再將此內(nèi)存塊的第1個分配單元返回給調(diào)用函數(shù)。將此新內(nèi)存塊插入 內(nèi)存塊鏈表的頭部的原因是該內(nèi)存塊還有很多可供分配的自由分配單元(除非nGrowSize等于1,這應(yīng)該不太可能。因?yàn)閮?nèi)存池的含義就是一次性地從進(jìn)程 堆中申請一大塊內(nèi)存,以供后續(xù)的多次申請),放在頭部可以使得在下次收到內(nèi)存申請時,減少②處對內(nèi)存塊的遍歷時間。
可以用圖6-2的MemoryPool來展示MemoryPool::Alloc的過程。圖6-3是某個時刻MemoryPool的內(nèi)部狀態(tài)。
因 為MemoryPool的內(nèi)存塊鏈表不為空,因此會遍歷其內(nèi)存塊鏈表。又因?yàn)榈?個內(nèi)存塊里有自由的分配單元,所以會從第1個內(nèi)存塊中分配。檢查 nFirst,其值為m,這時pBlock->aData+(pBlock->nFirst*nUnitSize)定位到編號為m的自由分配 單元的起始位置(用pFree表示)。在返回pFree之前,需要修改此內(nèi)存塊的維護(hù)信息。首先將nFree遞減1,然后取得pFree處開始的頭兩個字 節(jié)的值(需要說明的是,這里aData處值為k。其實(shí)不是這一個字節(jié)。而是以aData和緊跟其后的另外一個字節(jié)合在一起構(gòu)成的一個USHORT的值,不 可誤會)。發(fā)現(xiàn)為k,這時修改pBlock的nFirst為k。然后,返回pFree。此時MemoryPool的結(jié)構(gòu)如圖6-4所示。
可以看到,原來的第1個可供分配的單元(m編號處)已經(jīng)顯示為被分配的狀態(tài)。而pBlock的nFirst已經(jīng)指向原來m單元下一個自由分配單元的編號,即k。
(3)MemoryPool回收內(nèi)存時:
void MemoryPool::Free( void* pFree ) |
如前所述,回收分配單元時,可能會將整個內(nèi)存塊返回給進(jìn)程堆,也可能將被回收分配單元所屬的內(nèi)存塊移至內(nèi)存池的內(nèi)存塊鏈表的頭部。這兩個操作都需要修改鏈表結(jié)構(gòu)。這時需要知道該內(nèi)存塊在鏈表中前一個位置的內(nèi)存塊。
①處遍歷內(nèi)存池的內(nèi)存塊鏈表,確定該待回收分配單元(pFree)落在哪一個內(nèi)存塊的指針范圍內(nèi),通過比較指針值來確定。
運(yùn) 行到②處,pMyBlock即找到的包含pFree所指向的待回收分配單元的內(nèi)存塊(當(dāng)然,這時應(yīng)該還需要檢查pMyBlock為NULL時的情形,即 pFree不屬于此內(nèi)存池的范圍,因此不能返回給此內(nèi)存池,讀者可以自行加上)。這時將pMyBlock的nFree遞增1,表示此內(nèi)存塊的自由分配單元 多了一個。
③處用來修改該內(nèi)存塊的自由分配單元鏈表的信息,它將這個待回收分配單元的頭兩個字節(jié)的值指向該內(nèi)存塊原來的第一個可分配的自由分配單元的編號。
④處將pMyBlock的nFirst值改變?yōu)橹赶蜻@個待回收分配單元的編號,其編號通過計(jì)算此單元的起始位置相對pMyBlock的aData位置的差值,然后除以步長(nUnitSize)得到。
實(shí) 質(zhì)上,③和④兩步的作用就是將此待回收分配單元"真正回收"。值得注意的是,這兩步實(shí)際上是使得此回收單元成為此內(nèi)存塊的下一個可分配的自由分配單元,即 將它放在了自由分配單元鏈表的頭部。注意,其內(nèi)存地址并沒有發(fā)生改變。實(shí)際上,一個分配單元的內(nèi)存地址無論是在分配后,還是處于自由狀態(tài)時,一直都不會變 化。變化的只是其狀態(tài)(已分配/自由),以及當(dāng)其處于自由狀態(tài)時在自由分配單元鏈表中的位置。
⑤處檢查當(dāng)回收完畢后,包含此回收單元的內(nèi)存塊的所有單元是否都處于自由狀態(tài),且此內(nèi)存是否處于內(nèi)存塊鏈表的頭部。如果是,將此內(nèi)存塊整個的返回給進(jìn)程堆,同時修改內(nèi)存塊鏈表結(jié)構(gòu)。
注 意,這里在判斷一個內(nèi)存塊的所有單元是否都處于自由狀態(tài)時,并沒有遍歷其所有單元,而是判斷nFree乘以nUnitSize是否等于nSize。 nSize是內(nèi)存塊中所有分配單元的大小,而不包括頭部MemoryBlock結(jié)構(gòu)體的大小。這里可以看到其用意,即用來快速檢查某個內(nèi)存塊中所有分配單 元是否全部處于自由狀態(tài)。因?yàn)橹恍杞Y(jié)合nFree和nUnitSize來計(jì)算得出結(jié)論,而無須遍歷和計(jì)算所有自由狀態(tài)的分配單元的個數(shù)。
另 外還需注意的是,這里并不能比較nFree與nInitSize或nGrowSize的大小來判斷某個內(nèi)存塊中所有分配單元都為自由狀態(tài),這是因?yàn)榈?次 分配的內(nèi)存塊(分配單元個數(shù)為nInitSize)可能被移到鏈表的后面,甚至可能在移到鏈表后面后,因?yàn)槟硞€時間其所有單元都處于自由狀態(tài)而被整個返回 給進(jìn)程堆。即在回收分配單元時,無法判定某個內(nèi)存塊中的分配單元個數(shù)到底是nInitSize還是nGrowSize,也就無法通過比較nFree與 nInitSize或nGrowSize的大小來判斷一個內(nèi)存塊的所有分配單元是否都為自由狀態(tài)。
以上面分配后的內(nèi)存池狀態(tài)作為例子,假設(shè)這時第2個內(nèi)存塊中的最后一個單元需要回收(已被分配,假設(shè)其編號為m,pFree指針指向它),如圖6-5所示。
不 難發(fā)現(xiàn),這時nFirst的值由原來的0變?yōu)閙。即此內(nèi)存塊下一個被分配的單元是m編號的單元,而不是0編號的單元(最先分配的是最新回收的單元,從這一 點(diǎn)看,這個過程與棧的原理類似,即先進(jìn)后出。只不過這里的"進(jìn)"意味著"回收",而"出"則意味著"分配")。相應(yīng)地,m的"下一個自由單元"標(biāo)記為0, 即內(nèi)存塊原來的"下一個將被分配出去的單元",這也表明最近回收的分配單元被插到了內(nèi)存塊的"自由分配單元鏈表"的頭部。當(dāng)然,nFree遞增1。
處理至⑥處之前,其狀態(tài)如圖6-6所示。
這 里需要注意的是,雖然pFree被"回收",但是pFree仍然指向m編號的單元,這個單元在回收過程中,其頭兩個字節(jié)被覆寫,但其他部分的內(nèi)容并沒有改 變。而且從整個進(jìn)程的內(nèi)存使用角度來看,這個m編號的單元的狀態(tài)仍然是"有效的"。因?yàn)檫@里的"回收"只是回收給了內(nèi)存池,而并沒有回收給進(jìn)程堆,因此程 序仍然可以通過pFree訪問此單元。但是這是一個很危險的操作,因?yàn)槭紫仍搯卧诨厥者^程中頭兩個字節(jié)已被覆寫,并且該單元可能很快就會被內(nèi)存池重新分 配。因此回收后通過pFree指針對這個單元的訪問都是錯誤的,讀操作會讀到錯誤的數(shù)據(jù),寫操作則可能會破壞程序中其他地方的數(shù)據(jù),因此需要格外小心。
接著,需要判斷該內(nèi)存塊的內(nèi)部使用情況,及其在內(nèi)存塊鏈表中的位置。如果該內(nèi)存塊中省略號"……"所表示的其他部分中還有被分配的單元,即nFree乘以nUnitSize不等于nSize。因?yàn)榇藘?nèi)存塊不在鏈表頭,因此還需要將其移到鏈表頭部,如圖6-7所示。
如果該內(nèi)存塊中省略號"……"表示的其他部分中全部都是自由分配單元,即nFree乘以nUnitSize等于nSize。因?yàn)榇藘?nèi)存塊不在鏈表頭,所以此時需要將此內(nèi)存塊整個回收給進(jìn)程堆,回收后內(nèi)存池的結(jié)構(gòu)如圖6-8所示。
一個內(nèi)存塊在申請后會初始化,主要是為了建立最初的自由分配單元鏈表,下面是其詳細(xì)代碼:
MemoryBlock::MemoryBlock (USHORT nTypes, USHORT nUnitSize) |
這里可以看到,①處pData的初值是 aData,即0編號單元。但是②處的循環(huán)中i卻是從1開始,然后在循環(huán)內(nèi)部的③處將pData的頭兩個字節(jié)值置為i。即0號單元的頭兩個字節(jié)值為1,1 號單元的頭兩個字節(jié)值為2,一直到(nTypes-2)號單元的頭兩個字節(jié)值為(nTypes-1)。這意味著內(nèi)存塊初始時,其自由分配單元鏈表是從0號 開始。依次串聯(lián),一直到倒數(shù)第2個單元指向最后一個單元。
還需要注意的是,在其初始化列表中,nFree初始 化為nTypes-1(而不是nTypes),nFirst初始化為1(而不是0)。這是因?yàn)榈?個單元,即0編號單元構(gòu)造完畢后,立刻會被分配。另外注 意到最后一個單元初始并沒有設(shè)置頭兩個字節(jié)的值,因?yàn)樵搯卧跏荚诒緝?nèi)存塊中并沒有下一個自由分配單元。但是從上面例子中可以看到,當(dāng)最后一個單元被分配 并回收后,其頭兩個字節(jié)會被設(shè)置。
圖6-9所示為一個內(nèi)存塊初始化后的狀態(tài)。
當(dāng)內(nèi)存池析構(gòu)時,需要將內(nèi)存池的所有內(nèi)存塊返回給進(jìn)程堆:
MemoryPool::~MemoryPool() |
分 析內(nèi)存池的內(nèi)部原理后,本節(jié)說明如何使用它。從上面的分析可以看到,該內(nèi)存池主要有兩個對外接口函數(shù),即Alloc和Free。Alloc返回所申請的分 配單元(固定大小內(nèi)存),F(xiàn)ree則回收傳入的指針代表的分配單元的內(nèi)存給內(nèi)存池。分配的信息則通過MemoryPool的構(gòu)造函數(shù)指定,包括分配單元大 小、內(nèi)存池第1次申請的內(nèi)存塊中所含分配單元的個數(shù),以及內(nèi)存池后續(xù)申請的內(nèi)存塊所含分配單元的個數(shù)等。
綜上 所述,當(dāng)需要提高某些關(guān)鍵類對象的申請/回收效率時,可以考慮將該類所有生成對象所需的空間都從某個這樣的內(nèi)存池中開辟。在銷毀對象時,只需要返回給該內(nèi) 存池。"一個類的所有對象都分配在同一個內(nèi)存池對象中"這一需求很自然的設(shè)計(jì)方法就是為這樣的類聲明一個靜態(tài)內(nèi)存池對象,同時為了讓其所有對象都從這個內(nèi) 存池中開辟內(nèi)存,而不是缺省的從進(jìn)程堆中獲得,需要為該類重載一個new運(yùn)算符。因?yàn)橄鄳?yīng)地,回收也是面向內(nèi)存池,而不是進(jìn)程的缺省堆,還需要重載一個 delete運(yùn)算符。在new運(yùn)算符中用內(nèi)存池的Alloc函數(shù)滿足所有該類對象的內(nèi)存請求,而銷毀某對象則可以通過在delete運(yùn)算符中調(diào)用內(nèi)存池的 Free完成。
為 了測試?yán)脙?nèi)存池后的效果,通過一個很小的測試程序可以發(fā)現(xiàn)采用內(nèi)存池機(jī)制后耗時為297 ms。而沒有采用內(nèi)存池機(jī)制則耗時625 ms,速度提高了52.48%。速度提高的原因可以歸結(jié)為幾點(diǎn),其一,除了偶爾的內(nèi)存申請和銷毀會導(dǎo)致從進(jìn)程堆中分配和銷毀內(nèi)存塊外,絕大多數(shù)的內(nèi)存申請 和銷毀都由內(nèi)存池在已經(jīng)申請到的內(nèi)存塊中進(jìn)行,而沒有直接與進(jìn)程堆打交道,而直接與進(jìn)程堆打交道是很耗時的操作;其二,這是單線程環(huán)境的內(nèi)存池,可以看到 內(nèi)存池的Alloc和Free操作中并沒有加線程保護(hù)措施。因此如果類A用到該內(nèi)存池,則所有類A對象的創(chuàng)建和銷毀都必須發(fā)生在同一個線程中。但如果類A 用到內(nèi)存池,類B也用到內(nèi)存池,那么類A的使用線程可以不必與類B的使用線程是同一個線程。
另外,在第1章中已經(jīng)討論過,因?yàn)閮?nèi)存池技術(shù)使得同類型的對象分布在相鄰的內(nèi)存區(qū)域,而程序會經(jīng)常對同一類型的對象進(jìn)行遍歷操作。因此在程序運(yùn)行過程中發(fā)生的缺頁應(yīng)該會相應(yīng)少一些,但這個一般只能在真實(shí)的復(fù)雜應(yīng)用環(huán)境中進(jìn)行驗(yàn)證。
|
內(nèi) 存的申請和釋放對一個應(yīng)用程序的整體性能影響極大,甚至在很多時候成為某個應(yīng)用程序的瓶頸。消除內(nèi)存申請和釋放引起的瓶頸的方法往往是針對內(nèi)存使用的實(shí)際 情況提供一個合適的內(nèi)存池。內(nèi)存池之所以能夠提高性能,主要是因?yàn)樗軌蚶脩?yīng)用程序的實(shí)際內(nèi)存使用場景中的某些"特性"。比如某些內(nèi)存申請與釋放肯定發(fā) 生在一個線程中,某種類型的對象生成和銷毀與應(yīng)用程序中的其他類型對象要頻繁得多,等等。針對這些特性,可以為這些特殊的內(nèi)存使用場景提供量身定做的內(nèi)存 池。這樣能夠消除系統(tǒng)提供的缺省內(nèi)存機(jī)制中,對于該實(shí)際應(yīng)用場景中的不必要的操作,從而提升應(yīng)用程序的整體性能。
|
馮 宏華,清華大學(xué)計(jì)算機(jī)科學(xué)與技術(shù)系碩士。IBM 中國開發(fā)中心高級軟件工程師。 2003 年 12 月加入 IBM 中國開發(fā)中心,主要從事 IBM 產(chǎn)品的開發(fā)、性能優(yōu)化等工作。興趣包括 C/C++ 應(yīng)用程序性能調(diào)優(yōu),Windows 應(yīng)用程序開發(fā),Web 應(yīng)用程序開發(fā)等。 |
||
|
徐 瑩,山東大學(xué)計(jì)算機(jī)科學(xué)與技術(shù)系碩士。2003 年 4 月加入 IBM 中國開發(fā)中心,現(xiàn)任 IBM 中國開發(fā)中心開發(fā)經(jīng)理,一直從事IBM軟件產(chǎn)品在多個操作系統(tǒng)平臺上的開發(fā)工作。曾參與 IBM 產(chǎn)品在 Windows 和 Linux 平臺上的性能優(yōu)化工作,對 C/C++ 編程語言和跨平臺的大型軟件系統(tǒng)的開發(fā)有較豐富的經(jīng)驗(yàn)。 |
||
|
程 遠(yuǎn),北京大學(xué)計(jì)算機(jī)科學(xué)與技術(shù)系碩士。IBM 中國開發(fā)中心高級軟件工程師。2003 年加入 IBM 中國開發(fā)中心,主要從事IBM Productivity Tools 產(chǎn)品的開發(fā)、性能優(yōu)化等工作。興趣包括 C/C++ 編程語言,軟件性能工程,Windows/Linux 平臺性能測試優(yōu)化工具等。 |
||
|
汪 磊,北京航空航天大學(xué)計(jì)算機(jī)科學(xué)與技術(shù)系碩士,目前是 IBM 中國軟件開發(fā)中心高級軟件工程師。從 2002 年 12 月加入 IBM 中國開發(fā)中心至今一直從事旨在提高企業(yè)生產(chǎn)效率的應(yīng)用軟件開發(fā)。興趣包括 C\C++ 應(yīng)用程序的性能調(diào)優(yōu),Java 應(yīng)用程序的性能調(diào)優(yōu)。 |
||
首先,C++標(biāo)準(zhǔn)中提到,一個編譯單元[translation
unit]是指一個.cpp文件以及它所include的所有.h文件,.h文件里的代碼將會被擴(kuò)展到包含它的.cpp文件里,然后編譯器編譯該.cpp
文件為一個.obj文件,后者擁有PE[Portable
Executable,即windows可執(zhí)行文件]文件格式,并且本身包含的就已經(jīng)是二進(jìn)制碼,但是,不一定能夠執(zhí)行,因?yàn)椴⒉槐WC其中一定有main
函數(shù)。當(dāng)編譯器將一個工程里的所有.cpp文件以分離的方式編譯完畢后,再由連接器(linker)進(jìn)行連接成為一個.exe文件。
舉個例子:
//---------------test.h-------------------//
void f();//這里聲明一個函數(shù)f
//---------------test.cpp--------------//
#i nclude”test.h”
void f()
{
…//do something
} //這里實(shí)現(xiàn)出test.h中聲明的f函數(shù)
//---------------main.cpp--------------//
#i nclude”test.h”
int main()
{
f(); //調(diào)用f,f具有外部連接類型
}
在
這個例子中,test.
cpp和main.cpp各被編譯成為不同的.obj文件[姑且命名為test.obj和main.obj],在main.cpp中,調(diào)用了f函數(shù),然而
當(dāng)編譯器編譯main.cpp時,它所僅僅知道的只是main.cpp中所包含的test.h文件中的一個關(guān)于void
f();的聲明,所以,編譯器將這里的f看作外部連接類型,即認(rèn)為它的函數(shù)實(shí)現(xiàn)代碼在另一個.obj文件中,本例也就是test.obj,也就是
說,main.obj中實(shí)際沒有關(guān)于f函數(shù)的哪怕一行二進(jìn)制代碼,而這些代碼實(shí)際存在于test.cpp所編譯成的test.obj中。在
main.obj中對f的調(diào)用只會生成一行call指令,像這樣:
call f [C++中這個名字當(dāng)然是經(jīng)過mangling[處理]過的]
在
編譯時,這個call指令顯然是錯誤的,因?yàn)閙ain.obj中并無一行f的實(shí)現(xiàn)代碼。那怎么辦呢?這就是連接器的任務(wù),連接器負(fù)責(zé)在其它的.obj中
[本例為test.obj]尋找f的實(shí)現(xiàn)代碼,找到以后將call
f這個指令的調(diào)用地址換成實(shí)際的f的函數(shù)進(jìn)入點(diǎn)地址。需要注意的是:連接器實(shí)際上將工程里的.obj“連接”成了一個.exe文件,而它最關(guān)鍵的任務(wù)就是
上面說的,尋找一個外部連接符號在另一個.obj中的地址,然后替換原來的“虛假”地址。
這個過程如果說的更深入就是:
call f這行指令其實(shí)并不是這樣的,它實(shí)際上是所謂的stub,也就是一個
jmp
0x23423[這個地址可能是任意的,然而關(guān)鍵是這個地址上有一行指令來進(jìn)行真正的call
f動作。也就是說,這個.obj文件里面所有對f的調(diào)用都jmp向同一個地址,在后者那兒才真正”call”f。這樣做的好處就是連接器修改地址時只要對
后者的call
XXX地址作改動就行了。但是,連接器是如何找到f的實(shí)際地址的呢[在本例中這處于test.obj中],因?yàn)?obj于.exe的格式都是一樣的,在這
樣的文件中有一個符號導(dǎo)入表和符號導(dǎo)出表[import table和export
table]其中將所有符號和它們的地址關(guān)聯(lián)起來。這樣連接器只要在test.obj的符號導(dǎo)出表中尋找符號f[當(dāng)然C++對f作了mangling]的
地址就行了,然后作一些偏移量處理后[因?yàn)槭菍蓚€.obj文件合并,當(dāng)然地址會有一定的偏移,這個連接器清楚]寫入main.obj中的符號導(dǎo)入表中f
所占有的那一項(xiàng)。
這就是大概的過程。其中關(guān)鍵就是:
編譯main.cpp時,編譯器不知道f的實(shí)現(xiàn),所有當(dāng)碰到對它的調(diào)用時只是給出一個指示,指示連接器應(yīng)該為它尋找f的實(shí)現(xiàn)體。這也就是說main.obj中沒有關(guān)于f的任何一行二進(jìn)制代碼。
編譯test.cpp時,編譯器找到了f的實(shí)現(xiàn)。于是乎f的實(shí)現(xiàn)[二進(jìn)制代碼]出現(xiàn)在test.obj里。
連接時,連接器在test.obj中找到f的實(shí)現(xiàn)代碼[二進(jìn)制]的地址[通過符號導(dǎo)出表]。然后將main.obj中懸而未決的call XXX地址改成f實(shí)際的地址。
完成。
然而,對于模板,你知道,模板函數(shù)的代碼其實(shí)并不能直接編譯成二進(jìn)制代碼,其中要有一個“具現(xiàn)化”的過程。舉個例子:
//----------main.cpp------//
template<class T>
void f(T t)
{}
int main()
{
…//do something
f(10); //call f<int> 編譯器在這里決定給f一個f<int>的具現(xiàn)體
…//do other thing
}
也就是說,如果你在main.cpp文件中沒有調(diào)用過f,f也就得不到具現(xiàn),從而main.obj中也就沒有關(guān)于f的任意一行二進(jìn)制代碼!!如果你這樣調(diào)用了:
f(10); //f<int>得以具現(xiàn)化出來
f(10.0); //f<double>得以具現(xiàn)化出來
這樣main.obj中也就有了f<int>,f<double>兩個函數(shù)的二進(jìn)制代碼段。以此類推。
然而具現(xiàn)化要求編譯器知道模板的定義,不是嗎?
看下面的例子:[將模板和它的實(shí)現(xiàn)分離]
//-------------test.h----------------//
template<class T>
class A
{
public:
void f(); //這里只是個聲明
};
//---------------test.cpp-------------//
#i nclude”test.h”
template<class T>
void A<T>::f() //模板的實(shí)現(xiàn),但注意:不是具現(xiàn)
{
…//do something
}
//---------------main.cpp---------------//
#i nclude”test.h”
int main()
{
A<int> a;
a. f(); //編譯器在這里并不知道A<int>::f的定義,因?yàn)樗辉趖est.h里面
//于是編譯器只好寄希望于連接器,希望它能夠在其他.obj里面找到
//A<int>::f的實(shí)現(xiàn)體,在本例中就是test.obj,然而,后者中真有A<int>::f的
//二進(jìn)制代碼嗎?NO!!!因?yàn)镃++標(biāo)準(zhǔn)明確表示,當(dāng)一個模板不被用到的時
//侯它就不該被具現(xiàn)出來,test.cpp中用到了A<int>::f了嗎?沒有!!所以實(shí)
//際上test.cpp編譯出來的test.obj文件中關(guān)于A::f的一行二進(jìn)制代碼也沒有
//于是連接器就傻眼了,只好給出一個連接錯誤
//
但是,如果在test.cpp中寫一個函數(shù),其中調(diào)用A<int>::f,則編譯器會將其//具現(xiàn)出來,因?yàn)樵谶@個點(diǎn)上[test.cpp
中],編譯器知道模板的定義,所以能//夠具現(xiàn)化,于是,test.obj的符號導(dǎo)出表中就有了A<int>::f這個符號的地
//址,于是連接器就能夠完成任務(wù)。
}
關(guān)鍵是:在分離式編譯的環(huán)境下,編譯器編譯某一個.cpp文件時并不知道另一個.cpp文件的存在,也不會去查找[當(dāng)遇到未決符號時它會寄希望于連 接器]。這種模式在沒有模板的情況下運(yùn)行良好,但遇到模板時就傻眼了,因?yàn)槟0鍍H在需要的時候才會具現(xiàn)化出來,所以,當(dāng)編譯器只看到模板的聲明時,它不能 具現(xiàn)化該模板,只能創(chuàng)建一個具有外部連接的符號并期待連接器能夠?qū)⒎柕牡刂窙Q議出來。然而當(dāng)實(shí)現(xiàn)該模板的.cpp文件中沒有用到模板的具現(xiàn)體時,編譯器 懶得去具現(xiàn),所以,整個工程的.obj中就找不到一行模板具現(xiàn)體的二進(jìn)制代碼,于是連接器也黔
/////////////////////////////////
http://dev.csdn.net/develop/article/19/19587.shtm
C++模板代碼的組織方式 ——包含模式(Inclusion Model) 選擇自 sam1111 的 Blog
關(guān)鍵字 Template Inclusion Model
出處 C++ Template: The Complete Guide
說明:本文譯自《C++ Template: The Complete Guide》一書的第6章中的部分內(nèi)容。最近看到C++論壇上常有關(guān)于模板的包含模式的帖子,聯(lián)想到自己初學(xué)模板時,也為類似的問題困惑過,因此翻譯此文,希望對初學(xué)者有所幫助。
模板代碼有幾種不同的組織方式,本文介紹其中最流行的一種方式:包含模式。
鏈接錯誤
大多數(shù)C/C++程序員向下面這樣組織他們的非模板代碼:
·類和其他類型全部放在頭文件中,這些頭文件具有.hpp(或者.H, .h, .hh, .hxx)擴(kuò)展名。
·對于全局變量和(非內(nèi)聯(lián))函數(shù),只有聲明放在頭文件中,而定義放在點(diǎn)C文件中,這些文件具有.cpp(或者.C, .c, .cc, .cxx)擴(kuò)展名。
這種組織方式工作的很好:它使得在編程時可以方便地訪問所需的類型定義,并且避免了來自鏈接器的“變量或函數(shù)重復(fù)定義”的錯誤。
由于以上組織方式約定的影響,模板編程新手往往會犯一個同樣的錯誤。下面這一小段程序反映了這種錯誤。就像對待“普通代碼”那樣,我們在頭文件中定義模板:
// basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template <typename T>
void print_typeof (T const&);
#endif // MYFIRST_HPP
print_typeof()聲明了一個簡單的輔助函數(shù)用來打印一些類型信息。函數(shù)的定義放在點(diǎn)C文件中:
// basics/myfirst.cpp
#i nclude <iostream>
#i nclude <typeinfo>
#i nclude "myfirst.hpp"
// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{
std::cout << typeid(x).name() << std::endl;
}
這個例子使用typeid操作符來打印一個字符串,這個字符串描述了傳入的參數(shù)的類型信息。
最后,我們在另外一個點(diǎn)C文件中使用我們的模板,在這個文件中模板聲明被#i nclude:
// basics/myfirstmain.cpp
#i nclude "myfirst.hpp"
// use of the template
int main()
{
double ice = 3.0;
print_typeof(ice); // call function template for type double
}
大部分C++編譯器(Compiler)很可能會接受這個程序,沒有任何問題,但是鏈接器(Linker)大概會報告一個錯誤,指出缺少函數(shù)print_typeof()的定義。
這個錯誤的原因在于,模板函數(shù)print_typeof()的定義還沒有被具現(xiàn)化(instantiate)。為了具現(xiàn)化一個模板,編譯器必須知道 哪一個定義應(yīng)該被具現(xiàn)化,以及使用什么樣的模板參數(shù)來具現(xiàn)化。不幸的是,在前面的例子中,這兩組信息存在于分開編譯的不同文件中。因此,當(dāng)我們的編譯器看 到對print_typeof()的調(diào)用,但是沒有看到此函數(shù)為double類型具現(xiàn)化的定義時,它只是假設(shè)這樣的定義在別處提供,并且創(chuàng)建一個那個定義 的引用(鏈接器使用此引用解析)。另一方面,當(dāng)編譯器處理myfirst.cpp時,該文件并沒有任何指示表明它必須為它所包含的特殊參數(shù)具現(xiàn)化模板定 義。
頭文件中的模板
解決上面這個問題的通用解法是,采用與我們使用宏或者內(nèi)聯(lián)函數(shù)相同的方法:我們將模板的定義包含進(jìn)聲明模板的頭文件中。對于我們的例子,我們可以通 過將#i nclude "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一個使用我們的模板的點(diǎn)C文件中包含myfirst.cpp文件,來達(dá)到目 的。當(dāng)然,還有第三種方法,就是刪掉myfirst.cpp文件,并重寫myfirst.hpp文件,使它包含所有的模板聲明與定義:
// basics/myfirst2.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
#i nclude <iostream>
#i nclude <typeinfo>
// declaration of template
template <typename T>
void print_typeof (T const&);
// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{
std::cout << typeid(x).name() << std::endl;
}
#endif // MYFIRST_HPP
這種組織模板代碼的方式就稱作包含模式。經(jīng)過這樣的調(diào)整,你會發(fā)現(xiàn)我們的程序已經(jīng)能夠正確編譯、鏈接、執(zhí)行了。
從這個方法中我們可以得到一些觀察結(jié)果。最值得注意的一點(diǎn)是,這個方法在相當(dāng)程度上增加了包含myfirst.hpp的開銷。在這個例子中,這種開 銷并不是由模板定義自身的尺寸引起的,而是由這樣一個事實(shí)引起的,即我們必須包含我們的模板用到的頭文件,在這個例子中 是<iostream>和<typeinfo>。你會發(fā)現(xiàn)這最終導(dǎo)致了成千上萬行的代碼,因?yàn)橹T 如<iostream>這樣的頭文件也包含了和我們類似的模板定義。
這在實(shí)踐中確實(shí)是一個問題,因?yàn)樗黾恿司幾g器在編譯一個實(shí)際程序時所需的時間。我們因此會在以后的章節(jié)中驗(yàn)證其他一些可能的方法來解決這個問題。但無論如何,現(xiàn)實(shí)世界中的程序花一小時來編譯鏈接已經(jīng)是快的了(我們曾經(jīng)遇到過花費(fèi)數(shù)天時間來從源碼編譯的程序)。
拋開編譯時間不談,我們強(qiáng)烈建議如果可能盡量按照包含模式組織模板代碼。
另一個觀察結(jié)果是,非內(nèi)聯(lián)模板函數(shù)與內(nèi)聯(lián)函數(shù)和宏的最重要的不同在于:它并不會在調(diào)用端展開。相反,當(dāng)模板函數(shù)被具現(xiàn)化時,會產(chǎn)生此函數(shù)的一個新的
拷貝。由于這是一個自動的過程,編譯器也許會在不同的文件中產(chǎn)生兩個相同的拷貝,從而引起鏈接器報告一個錯誤。理論上,我們并不關(guān)心這一點(diǎn):這是編譯器設(shè)
計(jì)者應(yīng)當(dāng)關(guān)心的事情。實(shí)際上,大多數(shù)時候一切都運(yùn)轉(zhuǎn)正常,我們根本就不用處理這種狀況。然而,對于那些需要創(chuàng)建自己的庫的大型項(xiàng)目,這個問題偶爾會顯現(xiàn)出
來。
最后,需要指出的是,在我們的例子中,應(yīng)用于普通模板函數(shù)的方法同樣適用于模板類的成員函數(shù)和靜態(tài)數(shù)據(jù)成員,以及模板成員函數(shù)。
Definition:
A class is a pure interface if it meets the following requirements:
= 0") methods
and static methods (but see below for destructor).
Interface suffix.
An interface class can never be directly instantiated because of the pure virtual method(s) it declares. To make sure all implementations of the interface can be destroyed correctly, they must also declare a virtual destructor (in an exception to the first rule, this should not be pure). See Stroustrup, The C++ Programming Language, 3rd edition, section 12.4 for details.
Pros:
Tagging a class with the Interface suffix lets
others know that they must not add implemented methods or non
static data members. This is particularly important in the case of
multiple inheritance.
Additionally, the interface concept is already well-understood by
Java programmers.
Cons:
The Interface suffix lengthens the class name, which
can make it harder to read and understand. Also, the interface
property may be considered an implementation detail that shouldn't
be exposed to clients.
Decision:
A class may end with Interface only if it meets the
above requirements. We do not require the converse, however:
classes that meet the above requirements are not required to end
with Interface.
void placement() {
char *buf = new char[1000]; //pre-allocated buffer
string *p = new (buf) string("hi"); //placement new
string *q = new string("hi"); //ordinary heap allocation
cout<<
c_str()
<
<c_str();
}
:
placement new 表達(dá)式只是定位,不存在與其相對應(yīng)的delete,如果delete則選擇
delete[] buf。
operator new實(shí)際上總是以標(biāo)準(zhǔn)
extern void* operator new( size_t size )
{
if( size == 0 )
size = 1; // 這里保證像 new T[0] 這樣得語句也是可行的
void *last_alloc;
while( !(last_alloc = malloc( size )) )
{
if( _new_handler )
( *_new_handler )();
else
return 0;
}
return last_alloc;
}