2010年9月24日星期五

在Mac OS X下动态挂接内存分配的模块

在hero项目的优化过程中,我注意到Mac版本下内存使用比Windows来得多,并且运行时会有缓慢的增长。而Mac下不同于Windows的一点就是没有安装小块内存的管理模块。

前两天尝试让同事将Mac版本下挂上小块内存的管理模块tinymemmgr,但是遇到了一些问题。其它一些没有挂接tinymemmgr的模块会申请内存,交给我们的模块进行释放(似乎是carbon申请的,而释放的代码会链接到我们的主模块hero),接下来是中秋假期,这个工作就暂停了。

中秋这几天,我尝试做一下试验,看看这个优化的效果如何。因为我没有足够的源代码,所以想用动态挂接的方法去加载tinymemmgr。一个比较便捷的方法是启动时增加环境变量:

DYLD_INSERT_LIBRARIES=./tinymemmgr.dylib DYLD_FORCE_FLAT_NAMESPACE=1 启动程序 & 参数

在tinymemmgr.dylib中定义malloc、free等 同名函数即可,这些函数会替换标准库中的malloc等函数。

将tinymemmgr.dylib强行注入进去,DYLD_FORCE_FLAT_NAMEPSPACE必须设置为1,否则不能工作。因为Mac下分为两级的名字,tinymemmgr.dylib中的malloc和libSystem.dylib下的malloc其实不在一个名字空间下。

没想到的是,我测试通过了,但是hero工程却无法跑起来。花了很长的时间诊断,没有找到原因,只是知道卡在pthread的库中,我想应该是有关键的函数重名了。手册上给的警告本来就有使用了DYLD_FORCE_FLAT_NAMESPACE=1有可能导致程序无法启动。像hero这种使用了太多第三方库,尤其是有一些重量库的工程,跑不起来简直太正常了。我试验了一下,cmake也无法启动。

如果不能使用这个方法,我就只能用最后一招了,就是强行hack malloc的代码,注入指令来实现我希望的功能,但是这个方法的移植性很差。暂时没有好的办法,我打电话找陈拓琳求助,他查资料果然有一手,很快找到了一个方法无需使用DYLD_FORCE_FLAT_NAMESPACE=1这个参数即可完成注入,大概方法如下:

typedef struct interpose
{
    void *new_func;
    void *orig_func;
} interpose_t;


static const interpose_t interposers[] \
    __attribute__((section("__DATA, __interpose"))) =
{
    { (void *) my_malloc,   (void *) malloc   },
    { (void *) my_free,     (void *) free     },
}

在tinymemmgr.dylib中定义了这个数据段以后,dyld将知道用my_malloc/my_free函数去替换其他库引用的malloc/free入口。

使用这个方法,可以优雅无损的替换malloc/free等内存管理函数。当然,仅替换这些函数是不够的,还需要处理calloc/valloc/realloc/reallocf/posix_memalign等等函数。

接下来,就是我和系统库的斗争过程。有一些模块使用了malloc_zone_malloc这些方法,这很糟糕。这意味着我并没有100%的hook所有的内存申请(虽然可以这么做,但是这样未免太霸道了,不是一个游戏程序应该做的),我显然没必要给自己找这么多事情做。于是我只好增加了判断,根据标记判断内存是我申请的,还是malloc申请的。这并不是100%可靠,至少理论上不是,这只是权宜之计,不过就测试而言够了。

中途,我一度放弃了对malloc/free的接管,转而去替换new/delete这些C++的分配操作。使用nm可以查出,这些函数的导出符号是_Znam、_ZdaPv、_Znwm、_ZdlPv古怪名字。没事,不用担心,替换即可。但是我又遇到一些问题,编译器生成的代码并不是简单的通过使用上述的函数间接的调用malloc/free去分配释放内存,在某些时候(比如析构、异常处理的时),还是直接使用了malloc。这让我很头疼,于是只好放弃这个想法,转回去hook malloc/free。

标记斗争完毕以后,遇到了新问题。Mac是64位内核,并且有时需要使用SSE2的指令,这使得它要求内存必须是16字节对齐,这可真是一个噩耗:这意味即使分配4个字节,也要给它保留16个字节的空间以便对齐。因为我hook的是malloc而不是Ogre和我们自己工程的new/delete,所以无从知道对齐的要求(Ogre和我们自己的工程应该只要做到4字节对齐即可),只能按照最严格的来 - 就是16字节对齐。在解决了对齐问题以后,工程终于可以顺利的跑起来了。

测试结果是,内存可以略有节省(估计可以节约5-10M的样子),而且不会因为小片内存的频繁申请和释放导致碎片,对渲染效率会略有提升。总的来说,增加tinymemmgr还是有必要的,不过,更大的优化应该是在材质的管理上,目前频繁申请和释放材质的方法是一种糟糕的做法,降低了执行效率,提高内存miss的几率,并且对显卡驱动造成了负担,相比之下,材质管理的优化更加重要。tinymemmgr只要挂接到Ogre上就可以了(这是使用小块内存最频繁的模块),其他模块还是算了,这样可以回避和系统模块之间的冲突,而付出的代价是微乎其微的。

没有评论:

发表评论