如果我們自己本身用的crt提供的內存分配器,這個提升也算不得什么。問題是我們內部系統是有一個小內存管理器的,一般來說小內存分配的算法都大同小異,現成的實現也很多,比如linux內核的slab、SGI STL的分配器、ogre自帶的內存分配器,我們自己的內存分配器也和前面列舉的實現差不多。讓我們來看看這個項目有什么特別的吧。
一、使用方法
打開主頁,由于公司網絡禁止SVN從外部更新,所以只能下載了打包的源代碼。解壓后,看到有個doc目錄,進去,打開使用文檔,發現使用方法極為簡單:To use TCMalloc, just link TCMalloc into your application via the "-ltcmalloc" linker flag.再看算法,也沒什么特別的,還是和slab以及SGI STL分配器類似的算法。
unix環境居然只要鏈接這個tcmalloc庫就可以了!,太方便了,不過我手頭沒有linux環境,文檔上也沒提到windows環境怎么使用,
打開源代碼包,有個vs2003解決方案,打開,隨便挑選一個測試項目,查看項目屬性,發現僅僅有2點不同:
1、鏈接器命令行里多了
"..\..\release\libtcmalloc_minimal.lib",就是鏈接的時候依賴了這個內存優化庫。
2、鏈接器->輸入->強制符號引用 多了 __tcmalloc。
這樣就可以正確的使用tcmalloc庫了,測試了下,測試項目運行OK!
二、如何替換CRT的malloc
從前面的描述可知,項目強制引用了__tcmalloc, 搜索了測試代碼,沒發現用到_tcmalloc相關的函數和變量,這個選項應該是為了防止dll被優化掉(因為代碼里沒有什么地方用到這個dll的符號)。初看起來,鏈接這個庫后,不會影響任何現有代碼:我們沒有引用這個Lib庫的頭文件,也沒有使用過這個dll的導出函數。那么這個dll是怎么優化應用程序性能的呢?
實際調試,果然發現問題了,看看如下代碼
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進去,發現是
78134D09 E9 D2 37 ED 97 jmp `anonymous namespace'::LibcInfoWithPatchFunctions<8>::Perftools_malloc (100084E0h)
果然,從這里開始,就跳轉到libtcmalloc提供的Perftools_malloc了。
原來是通過API掛鉤來實現無縫替換系統自帶的malloc等crt函數的,而且還是通過大家公認的不推薦的改寫函數入口指令來實現的,一般只有在游戲外掛和金山詞霸之類的軟件才會用到這樣的掛鉤技術,
而且金山詞霸經常需要更新補丁解決不同系統兼容問題。
三、性能差別原因
如前面所述,tcmalloc確實用了很hacker的辦法來實現無縫的替換系統自帶的內存分配函數(本人在使用這類技術通常是用來干壞事的。。。),但是這也不足以解釋為什么它的效率比我們自己的好那么多。回到tcmalloc 的手冊,tcmalloc除了使用常規的小內存管理外,對多線程環境做了特殊處理,這和我原來見到的內存分配器大有不同,一般的內存分配器作者都會偷懶,把多線程問題扔給使用者,大多是加
個bool型的模板參數來表示是否是多線程環境,還美其名曰:可定制,末了還得吹噓下模板的優越性。
tcmalloc是怎么做的呢? 答案是每線程一個ThreadCache,大部分操作系統都會支持thread local storage 就是傳說中的TLS,這樣就可以實現每線程一個分配器了,
這樣,不同線程分配都是在各自的threadCache里分配的。我們的項目的分配器由于是多線程環境的,所以不管三七二十一,全都加鎖了,性能自然就低了。
僅僅是如此,還是不足以將tcmalloc和ptmalloc2分個高下,后者也是每個線程都有threadCache的。
關于這個問題,doc里有一段說明,原文貼出來:
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 也是通過tls來降低線程鎖,但是ptmalloc2各個線程的內存是獨立的,也就是說,第一個線程申請的內存,釋放的時候還是必須放到第一個線程池中(不可移動),這樣可能導致大量內存浪費。
四、代碼細節
1、無縫替換malloc等crt和系統分配函數。
前面提到tcmalloc會無縫的替換掉原有dll中的malloc,這就意味著使用tcmalloc的項目必須是 MD(多線程dll)或者MDd(多線程dll調試)。tcmalloc的dll定義了一個static TCMallocGuard module_enter_exit_hook;
的靜態變量,這個變量會在dll加載的時候先于DllMain運行,在這個類的構造函數,會運行PatchWindowsFunctions來掛鉤所有dll的 malloc、free、new等分配函數,這樣就達到了替換功能,除此之外,
為了保證系統兼容性,掛鉤API的時候還實現了智能分析指令,否則寫入第一條Jmp指令的時候可能會破環后續指令的完整性。
2、LibcInfoWithPatchFunctions 和ThreadCache。
LibcInfoWithPatchFunctions模板類包含tcmalloc實現的優化后的malloc等一系列函數。LibcInfoWithPatchFunctions的模板參數在我看來沒什么用處,tcmalloc默認可以掛鉤最多10個帶有malloc導出函數的庫(我想肯定是夠用了)。ThreadCache在每個線程都會有一個TLS對象:
__thread ThreadCache* ThreadCache::threadlocal_heap_。
3、可能的問題
設想下這樣一個情景:假如有一個dll 在tcmalloc之前加載,并且在分配了內存(使用crt提供的malloc),那么在加載tcmalloc后,tcmalloc會替換所有的free函數,然后,在某個時刻,
在前面的那個dll代碼中釋放該內存,這豈不是很危險。實際測試發現沒有任何問題,關鍵在這里:
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會通過span識別這個內存是否自己分配的,如果不是,tcmalloc會調用該dll原始對應函數(這個很重要)釋放。這樣就解決了這個棘手的問題。
五、其他
其實tcmalloc使用的每個技術點我從前都用過,但是我從來沒想過用API掛鉤來實現這樣一個有趣的內存優化庫(即使想過,也是一閃而過就否定了)。從tcmalloc得到靈感,結合常用的外掛技術,可以很輕松的開發一個獨立工具:這個工具可以掛載到指定進程進行內存優化,在我看來,這可能可以作為一個外掛輔助工具來優化那些
內存優化做的很差導致幀速很低的國產游戲。