首先,从性能上,File Line Tracer 所需的信息均来自编译期,运行时除了程序栈和hash之外不存在新的调用开销,而Dbghelp的信息则来自于运行时,开销自然比File Ln Tracer大得多。
然后,File Line Tracer需要define new,这会引入一些小麻烦,Dbghelp tracer则不需要这个东西。define new后面继续展开。
再然后,File Line Tracer只知道”分配内存的当前语句所在的文件和行号“,但DbgHelp还可以给出”分配内存的当前Call stack,更利于快速定位到错误的分配“。
define new最大的问题在于需要确保在new之前调用#define new DEBUG_NEW宏。
头文件包含关系比较乱的时候,这一点就比较难受。如果有预编译头,相对好办点,只要在预编译头的第一行加入这句话即可。但没有预编译头的时候,这就需要用户自己维护其正确性了。
否则,万一有.h里new,.cpp里delete这样的情况(或者相反),而define new又发生在这个.h之后,就极易发生误判的情况,某个new明明被删除了,但是没有记录下来,于是误报了内存泄露,或者某次删除发现删的不是相应的new……
头文件顺序真是C++永远的痛,伤不起啊伤不起……
无论哪个tracer,都增加了一些代码的开销。
如果内存分配器也是自己写的,这里就方便一点,内存分配的时候可以多在前分出一些小Cookie,在这些小Cookie里面记录所需的信息。然后需要这些信息的时候,只需要向前寻址若干字节,获取出Cookie,这样的性能是最高的,但会引入两个问题:
一,全套内存分配要自己做,dlmalloc、tlmalloc等成熟的第三方分配用不了了,这也是为什么我的例子代码里使用了独立的Tracer的原因。
二,要确认当前访问的内存是由本分配器分配出来的,一旦new与delete不配对,这种问题就会如雨后春笋般出现。一个不太好但是大部分情况下适用的方案是Cookie里记录一个魔数,每次访问时先判断魔数能不能对上,毕竟大部分场合下,内存里的数据正好对上魔数的可能性极低。但这种问题还是防不胜防。当你提供的并非全套解决方案,而是只是一个小模块,且会被其它人代码级而非二进制级引用时,这个问题就可能会变得愈发突出。
如果把Trace按”变种“章节所说的,改成全截获,永不销毁,那么接下来要面临的问题就是,一旦分配多起来,这内存占用就呼呼地往上涨了。
这时也有方案,就是把Tracer的改成不在本进程处理,而是将消息通过TCP连接发给其它应用程序去截获和处理。但这样的话,对DbgHelper来说,发送的信息就必须得包括Call stack的全文信息。否则另一个应用程序如果得到的只是Stack标记,要反解出来就要麻烦很多很多。好在发送线程可以做在另一个线程立,而每个Call stack全文信息获取一次以后可以缓存下来,Call stack全文信息的数量相对于分配数总归是少的,不是吗?
Tracer跨模块后,就会变成比较头疼的问题。
如果所有的模块全都是您自己维护的,那么您倒是可以保证您的Tracer公平地在每个模块里使用,不会出现问题。
但是如果您制作的是一个可发布的、相对精炼且功能相对单一的可分发程序包时,Tracer就要万分小心了。
首先,用户未必希望开启此模块的Tracer功能,这就需要提供专门的MEMLEAK DEBUG版本。
然后,如果用户使用了Tracer,就要确保由Tracer new出来的内存,也要由Tracer监督其delete,这里就需要守住”本模块new本模块删“的原则,但是原则嘛,自然是说着容易做着难了。你中枪了木有?
再然后,如果用户不想使用Tracer,或者不想让用户使用Tracer,就需要在分发版本的include文件中排除掉Tracer,而.h里相应使用的就需要用宏屏蔽。这个应该大家都是这么做的,权当废话好了。
最后,如果用户想用你的Tracer,相信我,这只是噩梦的开始……你永远无法知道用户会怎么使用你提供的库和接口……所以唯一能做的,只能是让他们无从选择。
跨模块是一个相当让人绞尽脑汁的特性,如果您自己完全可控的项目,建议还是不要在这个路上走得太远。毕竟,适应的才是最好的,您说呢?