虽然这名字很长,但其实就是一码事,试问你做内存跟踪不是为了看泄露?试问你看到了泄露和碎片不回去优化?哈哈
理论知识咱不具备,所以现实点,从实践出发好了。
我一向干点啥事,都是被勾引的,先是要解决问题,然后又觉得不够过瘾,就变成优化问题本身。这回的任务原本是内存泄露,我东一榔头,西一锤子的,却是满世界乱敲,敲到最后,原来的问题早湮灭了,剩下的,只是自己的问题而已。
内存问题由来已久,最早做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 (http://www.codeproject.com/KB/applications/visualleakdetector.aspx)是也,当然商业化的BoundsChecker也挺好,前提是你有破解和一台彪悍无比的机器。以及,vld和boundschecker不会自杀,好吧,我承认我这里都跑不起来,我人品应该没问题吧。。。。。。
自己写一个也不难,StackWalk64
函数给好了,抄抄代码也就搞定了,自己用用还是够的。
一般这个阶段是最消耗时间和耐性的,毕竟有了CallStack也还是要自己一行行的去review code,但同理,实际情况永远比你想象的复杂,比如我明明看到全局统计里面,C/C++内存没有涨,可是任务管理器里面已经涨了200MB了,oh my god,这又是怎么一回事。
是的,有一个重要问题是不能忘记的,这里不但有可控的c/c++,还有不可控的C#和.net。我就知道一个真实的事,有一帮人心比天高的开发了一套很高级的j2ee后台服务,号称。。。(此处省略5000字),上线前听说出事了,内存泄露,好吧,java的内存泄露,后来又听说解决了,怎么解决的呢,买了IBM的purify和一年高级服务,然后直接用高级服务从美国弄了个人过来purify了三天,价格?10w+。
GC我实在是不熟悉,只好又去求教Jeffrey Richter 的 《CLR Via C#》,然后被我发现了 CLR Profiler,所以说什么事情都怕楞的,我这个也是初哥蛮干,选项全开,CLR Profiler一开,挂了。内存一路飚涨,直到“Out of Memory”,无语。后来吧,学乖了只开个基本开关,做几次snapshot马上退出,看着内存从1G飚到3G,再飚下来,心里直念阿弥陀佛你老人家早啊。看看结果我也楞了,20寸的显示器完全不够大的,那位借个65寸的给我使使吧,最后没办法,数据拷回去那看电影那个28的屏幕做分析,勉强够啊也就。真不是一般人玩的起的。.Net还真是出了问题,有几个对象死活没有释放,而且还越来越多,貌似什么eventhandler,我的不懂。后来有人找了个商业的.net 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算法名声不显的原因吧。
hoard:http://prisms.cs.umass.edu/emery/index.php?page=hoard
nedmalloc:http://www.nedprod.com/programs/portable/nedmalloc/
这些都是基于C的,我们还有一个后起之秀,基于C++的google-perftools,据说性能还要彪悍一些,由于本人VC6,所以就。。。。。
goole-perftools:http://code.google.com/p/google-perftools/
至于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次数吧,待续。。。。。