這個坑其實很大很大。。。。。。
雖然這名字很長,但其實就是一碼事,試問你做記憶體跟蹤不是為了看洩露?試問你看到了洩露和碎片不回去優化?哈哈
理論知識咱不具備,是以現實點,從實踐出發好了。
我一向幹點啥事,都是被勾引的,先是要解決問題,然後又覺得不夠過瘾,就變成優化問題本身。這回的任務原本是記憶體洩露,我東一榔頭,西一錘子的,卻是滿世界亂敲,敲到最後,原來的問題早湮滅了,剩下的,隻是自己的問題而已。
記憶體問題由來已久,最早做c++的時候,包裝的很好,這個東西其實不明顯,因為多半是new和delete,malloc和free顯得特别紮眼,泾渭分明,最大的麻煩也不過是從這裡new/malloc,去那裡delete/free,這種東西極少,反而很好抓出來暴打五十大闆。是以那會名義上boundschecker啥的用了一堆,實際上卻沒什麼技術活,都拼體力去了,反正就幾個點,我單步還不成麼。
這回卻是一個很好的鍛煉機會,因為遇到了一個類似photoshop的龐然大物,而且代碼從c到c++到c#,滿世界的malloc,new,GC,這要洩露起來,可謂是五毒俱全,聲色一時無兩啊。哈哈哈哈(被吓傻了)
Problem1:記憶體跟蹤
1:方法論
對于記憶體的跟蹤,其實辦法很多,我所知道的,可以歸為幾類
宏定義法:不管現代版的new還是古典版的malloc,#define吃遍天下無敵手啊,#define malloc mymalloc #define new mynew不就成了。
Hook法:其實new和malloc都提供有hook方法,《Windows核心程式設計》就提到了非著名的crtdbg庫(對于初級程式員,這東西少有人知啊)。
重載法:對于C++來說,其實還有個重載全局new的方法可以用。。。。。
工具法:著名工具BoundsChecker,processXP等。。。。。。。。
GC法:GC值得單獨拿出來說,因為你沒法幹涉他,但是又能找到很多工具,比如微軟的CLR Profiler,比如收費的.net memory profiler,以及各種重量級。。。
2:世界觀
最重要的問題,就是,你的問題是什麼,是以我稱之為世界觀問題,比如,就記憶體洩露而言,對于純C++程式而言(現代C++),new和delete以及設計良好的class可以極好的屏蔽記憶體洩露的問題,是以看到記憶體洩露,我認為合格的c++程式員應該首先考慮設計的合理性。而對于記憶體碎片問題,優雅高效的記憶體配置設定算法和一個設計合理的memory pool 才是終極兵器。
相對來說,核心問題,也就是最終決定你能不能解決問題的東西,是程式員的記憶體觀念,或者說對記憶體的了解,這方面推薦《windows核心程式設計》。至少總得知道使用者記憶體空間隻有2G,知道virtualAlloc,知道HeapAlloc,知道dll對記憶體的共享,知道記憶體碎片化的後果,知道tls。。。。。。
話說回來,一個全局的記憶體跟蹤器仍然是一大殺器,你說記憶體有問題,他說記憶體沒問題,你說你優化了,他說效率沒有變,你說我把記憶體全打出來了,吃了多少,費了多少,漏了多少,時間節省了多少,他。。。。。。。。。
最後我選了條通俗易懂的道道,#define malloc/free,當然,你還需要一個c級别的list,我想,就那麼多了,雖然一大片廢話隻得到這麼個結果。
3:實踐
實踐永遠比動腦子臆想複雜,比如我臆想着一個define+list就可以開殺的時候,發現還不是那麼回事。比如,當你需要跨越多層lib,在所有dll中統計全局記憶體的時候。事情就變複雜了,首先,你希望看到每個dll自己的記憶體,接着,你不想犧牲太多效率,然後,你還要結果能夠易于通路,最後,别忘了多線程同步。我想破了腦袋,才整出一個臨時解決方案的0.1beta版而已。
1:一個公用的memory庫,定義一個static變量用來指向memory list,這樣可以保證每個使用這個庫的dll擁有各自的memorylist。
2:一個全局的FileMapping,儲存所有dll中memorylist頭指針。這樣可以随時周遊全局記憶體,并且Filemapping是系統級别的可見,甚至可以在其他程序中通路。
3:簡單有效的資料結構(包括memoryinfo和globalmemoryinfo),良好的資料同步機制。
Problem 2:記憶體洩露
看到完整的記憶體統計資料的時候,我樂得那是一個合不攏嘴啊,這個刀基本上是磨了個1/3出來了,簡單砍砍柴,那是問題不大。
有了統計資訊,我們就可以解決一部分問題了,隻要再加上一個snapShot的功能,再加上compare一下,就可以針對問題,發現未釋放記憶體的大小和位址了。對于跟蹤大塊記憶體洩露,馬上就從蒙昧時代,進化到刀耕火種了。現在隻需要一個條件斷點,就可以把問題,跟個八九不離十出來。
但再好它也隻是個柴刀,裝備不夠好啊。可是加上了CallStack,那可就是電鋸和柴刀,白闆和暗金的距離了。這個東西其實有成品,著名的Visual Leak Detector
一般這個階段是最消耗時間和耐性的,畢竟有了CallStack也還是要自己一行行的去review code,但同理,實際情況永遠比你想象的複雜,比如我明明看到全局統計裡面,C/C++記憶體沒有漲,可是任務管理器裡面已經漲了200MB了,oh my god,這又是怎麼一回事。
是的,有一個重要問題是不能忘記的,這裡不但有可控的c/c++,還有不可控的C#和.net。我就知道一個真實的事,有一幫人心比天高的開發了一套很進階的j2ee背景服務,号稱。。。(此處省略5000字),上線前聽說出事了,記憶體洩露,好吧,java的記憶體洩露,後來又聽說解決了,怎麼解決的呢,買了IBM的purify和一年進階服務,然後直接用進階服務從美國弄了個人過來purify了三天,價格?10w+。
memory profiler 出來,馬上就好多了,那是腰也直了,眼睛也亮了,腿腳也好了啊,你看看人家,記憶體沒占多少,性能影響極少,自帶分析界面,可以snapShot,可以compare,還帶智能分析。敗家的clr profiler啊。
至此問題基本解決了,其實已經可以喝酒聊天,大家party了。剩下的全是我個人興趣。。。。。
Problem 3:記憶體碎片
碎片吧,其實也不算是特大問題,俗話說擠一擠總還有的,實在不行,我報錯重新開機總行吧,反正一個client,又不是Server。這也就是說說,以photoshop這種級别的client,記憶體開的那叫一個恐怖啊。我這随便操作了兩三下,得,malloc次數10w,這,這也太離譜了點。。。。。這不是把可憐的2G空間玩死裡捏嗎。
對于普通程式不太重要的碎片問題,在一跑就是幾個月的Server上,和對記憶體極度渴求的圖像,音樂,視訊程式中,就變成了暴風源頭,一不小心,那可就是一場台風。
對付碎片問題,絕招有兩個,一:用linux,二:用memory pool。
第一個答案其實很,怎麼說呢,沒辦法,做Server的人大概都知道,VC的Malloc效率極低,讓人無法忍受,而相反,linux的malloc幾乎是你能想到最新最快的,所有許多做linxu的人,很幹脆的就是直接用malloc/free,才懶得寫那勞什子的memory pool。
malloc其實也很多版本,最初的malloc算法也是種類繁多,各執一詞,但據說次從N年前,tcmalloc誕生之後,就王者歸來了一把,從此再無硝煙。隻是由于tcmalloc多線程比較差,是以之後才有linux的ptmalloc和通用版本的nedmalloc做了一些改進。可以毫不誇張的說,用nedmalloc/hoard用上手之後,一般的程式,也就不用優化來優化去了,也不用考慮啥memory pool了,我想,這也是現在malloc算法名聲不顯的原因吧。
至于Memory Pool,可謂仁者見仁,智者見智了,一般是用在嵌入式和Server上。其他地方,那就根據需求,量身定做,才會合乎需求,提高效率,不然就要畫虎不成反類犬了。用了還不如不用。而且對memory pool的管理算法,本人也是頭疼的緊,暫時還沒有一個特别有效的辦法,不知哪裡有達者可以教我。
Problem 4:記憶體優化
就我個人而言,是不願放棄碎片問題的,畢竟對于圖像程式,大量碎片傷害太大。是以我的最終方案,既不是替換malloc,也不是構造memory pool,而是第三種選擇,一個簡單的GC。
說是簡單,因為我隻做了兩件事情,記憶體重用和定期回收。free不再free,隻是設了個标志位,遇到還要malloc同樣size的記憶體時,直接return就好。而為了防止記憶體無限制增長,還做了定期回收,比如30秒或者一分鐘回收一次不再使用的記憶體。
經過測試,這個方案對于記憶體碎片問題是革命性的(純屬自誇),在30s回收條件下,malloc的次數整整降了一個數量級,原本10w次的malloc,隻需要不到8k,就能完成所需了。而記憶體占用,也并不比傻傻的malloc/free多出多少來,這也是圖像程式的性質決定的,往往都是開一些同樣大小的記憶體,malloc次數又是極高。
接下來的問題,就是比較了,這種方案除了減少碎片以外,到底能帶來多大的性能提高呢?
最後,希望能實作一個漂亮的GC算法,提高10倍左右的性能和降低2個數量級的malloc/free次數吧,待續。。。。。