0CCh Blog

关于DLL加载和运行的性能优化总结

本文从三个方面总结了加快DLL加载和运行速度的方法,他们分别是:

  1. 使用Rebase和Bind。
  2. 按序号的方式导入函数。
  3. 使用预读取技术(chromium加载dll时使用了此项)。

首先,讨论一下使用Rebase和Bind,提高DLL加载和运行性能的方法。我们知道,在编译链接DLL的时候,连接器会给DLL一个加载基址,而这个加载基址对于DLL模块中的使用了硬编码地址的代码和数据是至关重要的。因为这些硬编码的地址都是连接器通过DLL加载基址计算出来的。

这样,当我们的DLL被加载到进程空间当中的时候,如果足够幸运的话,DLL正好被加载到连接器所指定的地址,那么那些硬编码的地址就是完全正确的,加载过程不需要其他额外的工作就能让DLL正确运行了。

但是,真实的情况并不是这么理想,因为我们的进程可能不得不加载许许多多DLL,这样就会导致,某些DLL预定的基址可能已经被其他DLL使用。因此,前者就不得不加载到其他地址。由此带来的影响也是显而易见的,硬编码的地址就会出错。为了不让这种错误发生,加载器也就不得不多做一些工作,就是重新矫正这些硬编码。这样,如果DLL里包含的硬编码地址越多,加载的速度就会越慢,从而导致DLL加载性能下降。

另一方面,由于DLL加载基址是不可预见的,所以DLL加载的时候,加载器会根据导入表搜索所有的导入函数,计算并且确定导入函数的正确地址。这个过程也会对DLL加载性能有一定影响。

微软已经考虑到了这个问题的优化方法,在SDK中为我们提供了Rebase和Bind工具。其中Rebase工具,可以合理安排进程中DLL的加载地址,并且修改到PE中,从而避免DLL加载时Rebase操作。这样,一旦我们确认了DLL的加载基址是不会发生改变,那么我们就可以使用Bind工具将导入表进行绑定,这样的好处就是加载器不需要在加载的时候去计算确定导入函数地址,因为这些地址以及被预设了。

需要注意的一点,即使你做了这么多的事情,也不能完全避免加载基址的冲突,例如使用ASLR的DLL,其加载的地址是会发生变化的。所以就不能保证Rebase和Bind的有效性。具体能优化多少性能,在不同的案例中可能结果不同,需要具体实验才能知道。

第二种优化DLL运行性能的方案就是使用序号而非名称的方式导入函数。这一点也就非常容易解释了,如果通过函数名确定导入函数地址,那么加载器就不得不对字符串进行比较,从而确定正确的函数地址,虽然DLL的导出表是按顺序字母排列的,并且查找方式也是二分查找,但是如果函数很多,这依然是个耗时的工作。例如,我机器上的MFC100.dll有14000多个导出函数,如果进程需要按照名称确定自己需要的函数是哪一个,那么工作量还是不小的吧。所以MFC100.dll很明智的使用了导出序号的方法,这样加载器计算导入函数的时候,就能够使用序号来确定函数地址了,也就是简单使用数组搜索,从而得到目标函数地址。

这种方法的优化效果同样也要根据具体情况而定,如果导入函数少,目标DLL导出的函数也非常少,那么这种优化应该是没有什么意义的。相反,如果需要导入函数很多,而且目标DLL也导出了很多函数,那么在想提高程序加载性能的时候,不妨试一试这个方案。

最后一个方法是Pre-Read技术,使用在chromium中的。这种方法的原理是将DLL预先存入系统缓存,从而减少Page Fault来达到提高性能的目的。这种优化主要针对的是进程冷启动加载DLL的情况。进程第一次启动的时候,加载所需的DLL,DLL会被MAP到内存空间,虽然如果查询这片MAP的内存,会发现确实是COMMIT状态。但是,实际上系统并没要保证这些内存在Working Set中。一般情况下,系统只会把你想要用到的内存加载到Working Set中,以节约物理内存。这样,当我们每次用到这个并没有对应的Working Set的虚拟内存的时候,就发生了Page Fault,系统这个时候才会把这些内存加载到Working Set。而Page Fault对性能的影响很是比较大的。所以Pre-Read技术就有了用武之地。

Chromium对Pre-Read实现的非常好,代码的具体位置是http://src.chromium.org/viewvc/chrome/trunk/src/chrome/app/image_pre_reader_win.cc 。代码中,分别针对XP和XP以上的系统使用了不同的方法让系统缓存目标DLL。在XP以上的系统中,代码简单的通过ReadFile将文件读取到内存中,然后释放内存,关闭文件句柄,就可以达到缓存目标DLL的目的。而在XP系统下,做法有些不同,它使用LoadLibraryEx函数,将文件Load到内存空间,然后尝试对每个Page进行读取操作,已达到让数据载入Working Set的目的。

以上是我所知道的三种加载DLL的优化方法,也可能还有更多更好的方法,有兴趣的可以一起讨论下。不过无论什么性能优化方法,都必须建立在实际项目的基础上,并且有科学的依据和论证,切不可只从理论上下结论来优化程序,纸上谈兵,这样很可能适得其反,让性能变得更糟。(例如这个例子里所提到的,看上去不错的优化可能还不如什么都不做:http://blogs.msdn.com/b/oldnewthing/archive/2004/12/17/317157.aspx)。至于说,如何得到程序运行数据用于总结出有效的优化方案,这里强烈推荐一款神器XPerf。实际上,也是因为使用了XPerf,才让我对性能优化产生了浓厚的兴趣!