昨天一個(gè)同事一大早在群里推薦了一個(gè)google project上的開(kāi)源內(nèi)存分配器(
http://code.google.com/p/google-perftools/),據(jù)說(shuō)google的很多產(chǎn)品都用到了這個(gè)內(nèi)存分配庫(kù),而且經(jīng)他測(cè)試,我們的游戲客戶(hù)端集成了這個(gè)最新內(nèi)存分配器后,F(xiàn)PS足足提高了將近10幀左右,這可是個(gè)了不起的提升,要知道3D組的兄弟忙了幾周也沒(méi)見(jiàn)這么大的性能提升。
如果我們自己本身用的crt提供的內(nèi)存分配器,這個(gè)提升也算不得什么。問(wèn)題是我們內(nèi)部系統(tǒng)是有一個(gè)小內(nèi)存管理器的,一般來(lái)說(shuō)小內(nèi)存分配的算法都大同小異,現(xiàn)成的實(shí)現(xiàn)也很多,比如linux內(nèi)核的slab、SGI STL的分配器、ogre自帶的內(nèi)存分配器,我們自己的內(nèi)存分配器也和前面列舉的實(shí)現(xiàn)差不多。讓我們來(lái)看看這個(gè)項(xiàng)目有什么特別的吧。
一、使用方法
打開(kāi)主頁(yè),由于公司網(wǎng)絡(luò)禁止SVN從外部更新,所以只能下載了打包的源代碼。解壓后,看到有個(gè)doc目錄,進(jìn)去,打開(kāi)使用文檔,發(fā)現(xiàn)使用方法極為簡(jiǎn)單:
To use TCMalloc, just link TCMalloc into your application via the
"-ltcmalloc" linker flag.再看算法,也沒(méi)什么特別的,還是和slab以及SGI STL分配器類(lèi)似的算法。
unix環(huán)境居然只要鏈接這個(gè)tcmalloc庫(kù)就可以了!,太方便了,不過(guò)我手頭沒(méi)有l(wèi)inux環(huán)境,文檔上也沒(méi)提到windows環(huán)境怎么使用,
打開(kāi)源代碼包,有個(gè)vs2003解決方案,打開(kāi),隨便挑選一個(gè)測(cè)試項(xiàng)目,查看項(xiàng)目屬性,發(fā)現(xiàn)僅僅有2點(diǎn)不同:
1、鏈接器命令行里多了
"..\..\release\libtcmalloc_minimal.lib",就是鏈接的時(shí)候依賴(lài)了這個(gè)內(nèi)存優(yōu)化庫(kù)。
2、鏈接器->輸入->強(qiáng)制符號(hào)引用 多了 __tcmalloc。
這樣就可以正確的使用tcmalloc庫(kù)了,測(cè)試了下,測(cè)試項(xiàng)目運(yùn)行OK!
二、如何替換CRT的malloc
從前面的描述可知,項(xiàng)目強(qiáng)制引用了__tcmalloc, 搜索了測(cè)試代碼,沒(méi)發(fā)現(xiàn)用到_tcmalloc相關(guān)的函數(shù)和變量,這個(gè)選項(xiàng)應(yīng)該是為了防止dll被優(yōu)化掉(因?yàn)榇a里沒(méi)有什么地方用到這個(gè)dll的符號(hào))。
初看起來(lái),鏈接這個(gè)庫(kù)后,不會(huì)影響任何現(xiàn)有代碼:我們沒(méi)有引用這個(gè)Lib庫(kù)的頭文件,也沒(méi)有使用過(guò)這個(gè)dll的導(dǎo)出函數(shù)。那么這個(gè)dll是怎么優(yōu)化應(yīng)用程序性能的呢?
實(shí)際調(diào)試,果然發(fā)現(xiàn)問(wèn)題了,看看如下代碼
void* pData = malloc(100);
00401085 6A 64 push 64h
00401087 FF 15 A4 20 40 00 call dword ptr [__imp__malloc (4020A4h)]
跟蹤 call malloc這句,step進(jìn)去,發(fā)現(xiàn)是
78134D09 E9 D2 37 ED 97 jmp `anonymous namespace'::LibcInfoWithPatchFunctions<8>::Perftools_malloc (100084E0h)
果然,從這里開(kāi)始,就跳轉(zhuǎn)到libtcmalloc提供的Perftools_malloc了。
原來(lái)是通過(guò)API掛鉤來(lái)實(shí)現(xiàn)無(wú)縫替換系統(tǒng)自帶的malloc等crt函數(shù)的,而且還是通過(guò)大家公認(rèn)的不推薦的改寫(xiě)函數(shù)入口指令來(lái)實(shí)現(xiàn)的,一般只有在游戲外掛和金山詞霸之類(lèi)的軟件才會(huì)用到這樣的掛鉤技術(shù),
而且金山詞霸經(jīng)常需要更新補(bǔ)丁解決不同系統(tǒng)兼容問(wèn)題。
三、性能差別原因
如前面所述,tcmalloc確實(shí)用了很hacker的辦法來(lái)實(shí)現(xiàn)無(wú)縫的替換系統(tǒng)自帶的內(nèi)存分配函數(shù)(本人在使用這類(lèi)技術(shù)通常是用來(lái)干壞事的。。。),但是這也不足以解釋為什么它的效率比我們自己的好那么多。
回到tcmalloc 的手冊(cè),tcmalloc除了使用常規(guī)的小內(nèi)存管理外,對(duì)多線程環(huán)境做了特殊處理,這和我原來(lái)見(jiàn)到的內(nèi)存分配器大有不同,一般的內(nèi)存分配器作者都會(huì)偷懶,把多線程問(wèn)題扔給使用者,大多是加
個(gè)bool型的模板參數(shù)來(lái)表示是否是多線程環(huán)境,還美其名曰:可定制,末了還得吹噓下模板的優(yōu)越性。
tcmalloc是怎么做的呢? 答案是每線程一個(gè)ThreadCache,大部分操作系統(tǒng)都會(huì)支持thread local storage 就是傳說(shuō)中的TLS,這樣就可以實(shí)現(xiàn)每線程一個(gè)分配器了,
這樣,不同線程分配都是在各自的threadCache里分配的。我們的項(xiàng)目的分配器由于是多線程環(huán)境的,所以不管三七二十一,全都加鎖了,性能自然就低了。
僅僅是如此,還是不足以將tcmalloc和ptmalloc2分個(gè)高下,后者也是每個(gè)線程都有threadCache的。
關(guān)于這個(gè)問(wèn)題,doc里有一段說(shuō)明,原文貼出來(lái):
ptmalloc2 also reduces lock contention by using per-thread arenas but
there is a big problem with ptmalloc2's use of per-thread arenas. In
ptmalloc2 memory can never move from one arena to another. This can
lead to huge amounts of wasted space.
大意是這樣的:ptmalloc2 也是通過(guò)tls來(lái)降低線程鎖,但是ptmalloc2各個(gè)線程的內(nèi)存是獨(dú)立的,也就是說(shuō),第一個(gè)線程申請(qǐng)的內(nèi)存,釋放的時(shí)候還是必須放到第一個(gè)線程池中(不可移動(dòng)),這樣可能導(dǎo)致大量?jī)?nèi)存浪費(fèi)。
四、代碼細(xì)節(jié)
1、無(wú)縫替換malloc等crt和系統(tǒng)分配函數(shù)。
前面提到tcmalloc會(huì)無(wú)縫的替換掉原有dll中的malloc,這就意味著使用tcmalloc的項(xiàng)目必須是 MD(多線程dll)或者M(jìn)Dd(多線程dll調(diào)試)。tcmalloc的dll定義了一個(gè)
static TCMallocGuard module_enter_exit_hook;
的靜態(tài)變量,這個(gè)變量會(huì)在dll加載的時(shí)候先于DllMain運(yùn)行,在這個(gè)類(lèi)的構(gòu)造函數(shù),會(huì)運(yùn)行PatchWindowsFunctions來(lái)掛鉤所有dll的 malloc、free、new等分配函數(shù),這樣就達(dá)到了替換功能,除此之外,
為了保證系統(tǒng)兼容性,掛鉤API的時(shí)候還實(shí)現(xiàn)了智能分析指令,否則寫(xiě)入第一條Jmp指令的時(shí)候可能會(huì)破環(huán)后續(xù)指令的完整性。
2、LibcInfoWithPatchFunctions 和ThreadCache。
LibcInfoWithPatchFunctions模板類(lèi)包含tcmalloc實(shí)現(xiàn)的優(yōu)化后的malloc等一系列函數(shù)。LibcInfoWithPatchFunctions的模板參數(shù)在我看來(lái)沒(méi)什么用處,tcmalloc默認(rèn)可以掛鉤
最多10個(gè)帶有malloc導(dǎo)出函數(shù)的庫(kù)(我想肯定是夠用了)。ThreadCache在每個(gè)線程都會(huì)有一個(gè)TLS對(duì)象:
__thread ThreadCache* ThreadCache::threadlocal_heap_。
3、可能的問(wèn)題
設(shè)想下這樣一個(gè)情景:假如有一個(gè)dll 在tcmalloc之前加載,并且在分配了內(nèi)存(使用crt提供的malloc),那么在加載tcmalloc后,tcmalloc會(huì)替換所有的free函數(shù),然后,在某個(gè)時(shí)刻,
在前面的那個(gè)dll代碼中釋放該內(nèi)存,這豈不是很危險(xiǎn)。實(shí)際測(cè)試發(fā)現(xiàn)沒(méi)有任何問(wèn)題,關(guān)鍵在這里:
span = Static::pageheap()->GetDescriptor(p);
if (!span) {
// span can be NULL because the pointer passed in is invalid
// (not something returned by malloc or friends), or because the
// pointer was allocated with some other allocator besides
// tcmalloc. The latter can happen if tcmalloc is linked in via
// a dynamic library, but is not listed last on the link line.
// In that case, libraries after it on the link line will
// allocate with libc malloc, but free with tcmalloc's free.
(*invalid_free_fn)(ptr); // Decide how to handle the bad free request
return;
}
tcmalloc會(huì)通過(guò)span識(shí)別這個(gè)內(nèi)存是否自己分配的,如果不是,tcmalloc會(huì)調(diào)用該dll原始對(duì)應(yīng)函數(shù)(這個(gè)很重要)釋放。這樣就解決了這個(gè)棘手的問(wèn)題。
五、其他
其實(shí)tcmalloc使用的每個(gè)技術(shù)點(diǎn)我從前都用過(guò),但是我從來(lái)沒(méi)想過(guò)用API掛鉤來(lái)實(shí)現(xiàn)這樣一個(gè)有趣的內(nèi)存優(yōu)化庫(kù)(即使想過(guò),也是一閃而過(guò)就否定了)。
從tcmalloc得到靈感,結(jié)合常用的外掛技術(shù),可以很輕松的開(kāi)發(fā)一個(gè)獨(dú)立工具:這個(gè)工具可以掛載到指定進(jìn)程進(jìn)行內(nèi)存優(yōu)化,在我看來(lái),這可能可以作為一個(gè)外掛輔助工具來(lái)優(yōu)化那些
內(nèi)存優(yōu)化做的很差導(dǎo)致幀速很低的國(guó)產(chǎn)游戲。
posted on 2010-07-10 17:32
feixuwu 閱讀(10074)
評(píng)論(14) 編輯 收藏 引用 所屬分類(lèi):
游戲開(kāi)發(fā)