性能分析工具的实现与应用

作为一名程序员,经常需要编写产品附加应用程序或主应用程序的第三方DLL。因此,作为开发者,需要确保代码不会降低主应用程序或产品的性能。通常,会使用像IBM Rational Product Suite或Bounds Checker这样的商业产品。但有时,这些商业工具并不适用,因为编写的代码(DLL)依赖于主应用程序,而主应用程序本身并不适合用于性能分析。主要目标是获取代码中每个函数的执行时间以及该函数被调用的次数。简而言之,想对代码进行影响分析,以便可以获取性能改进的输入。这样,就可以确保主应用程序或产品的总体性能得到维护。

与一位同样是经验丰富的C/C++程序员的朋友讨论了这个问题。他建议,Visual StudioC++编译器有一些标志可以用来编写特定于函数的代码。这些编译器标志是/Gh和/GH。/Gh标志会在每个方法或函数的开始处调用_penter函数,而/GH标志会在每个方法或函数的结束处调用_pexit函数。因此,如果在这些方法中编写一些代码来找出调用者函数,那么将收集堆栈跟踪信息。此外,如果编写代码在_penter函数中开始计时,并在_pexit函数中停止相应的计时器,那么将大致测量方法或函数执行所需的时间。但在编写任何代码之前,非常重要的是要理解_penter和_pexit函数。MSDN指出,_penter和_pexit函数不是任何库的一部分,而是由开发者提供定义。因此,决定编写DLL,在其中将提供_penter和_pexit的定义,同时这两个函数将从DLL中导出。原型如下:

C++ void __declspec ( naked ) _cdecl _penter( void ); void __declspec ( naked ) _cdecl _pexit( void );

这些方法被定义为__declspec(naked)和_cdecl,这意味着实现应该在入口处将所有寄存器的内容推入堆栈,并在退出时弹出未更改的内容。此外,函数体内部不能实例化对象,只能使用全局或静态变量。只能从函数体内部调用全局或静态方法。考虑到所有这些,创建了一个单例Profiler类,它将具有收集时间分析数据所需的方法。使用C++内联汇编功能来实现_penter和_pexit。示例实现如下:

extern " C" void __declspec ( naked ) _cdecl _penter( void ) { _asm { //Prolog instructions pushad //calculate the pointer to the return address by adding 4*8 bytes //(8 register values are pushed onto stack which must be removed) mov eax, esp add eax, 32 // retrieve return address from stack mov eax, dword ptr[eax] // subtract 5 bytes as instruction for call _penter // is 5 bytes long on 32-bit machines, e.g. E8 <00 00 00 00> sub eax, 5 // provide return address to recordFunctionCall push eax call enterFunc pop eax //Epilog instructions popad ret } }

实现中有趣的部分是如何使用非裸体全局函数enterFunc,将调用者函数的虚拟地址作为参数进行调用。编写Prolog指令后,通过从当前堆栈指针添加32字节来遍历堆栈。然后,通过指针操作从该虚拟地址获取返回地址。现在,从该地址减去5字节,将得到调用者函数体的虚拟地址。这个虚拟地址作为参数传递给全局函数,该函数进行进一步处理。时间保持部分是通过这种安排解决的。但是,函数名称呢?应该如何从任何函数体的虚拟地址获取函数名称?

通过使用DIA(Debug Interface Access)SDK解决了这个名称问题。DIA SDK有一个统一的模型,可以访问或查询PDB文件中的任何符号及其属性。因此,要使用此分析器,非常重要的是,DLL或EXE应该具有调试信息(PDB文件)。

在Profiler类中实现了getFunc,它可以根据给定的虚拟地址找出函数名称。过程如下:

Get the current process handle using the GetCurrentProcess function Get all the loaded modules of the current process using the EnumProcessModules function Check if the given virtual address belongs to any modules using its address space size and load address Get the module file path from the module handle if the given virtual address belongs to that module, using GetModuleFileNameEx Load the PDB file from the module file path using the loadDataForExe method of IDiaDataSource Use the openSession method of IDiaDataSource to get IDiaSession and use the put_loadAddress method of IDiaSession to setup the symbol database for the query Now, query the IDiaSession object using the findSymbolByVA method which would return IDiaSymbol , i.e., the function having the given virtual address in its body Get the function name from the IDiaSymbol object using the get_name method

使用代码

本文中描述的分析器使用DIA SDK,因此,如果想使用此分析器,则必须生成调试信息的项目(LIB/DLL/EXE)。要为项目生成调试信息,请转到相应的项目的“常规”属性页,该属性页位于“C/C++”选项卡下,并将“调试信息格式”设置为“程序数据库(/Zi)”。此外,对于“调试”属性页,该属性页位于“链接器”选项卡下,将“生成调试信息”设置为“是(/Debug)”。这两个设置将确保为要分析的项目创建PDB文件。

在将项目设置为生成PDB文件之后,在“命令行”属性页下的“附加选项”中设置/Gh和/GH标志,如下面所示。

在“输入”属性页下的“附加依赖项”中添加profiler.lib(分析器项目的导出库)。这对于分析器工作很重要,因为profiler.lib/dll具有_penter和_pexit的实现。

在“链接器”属性页的“附加库目录”中提供profiler.lib的路径。这个设置将取决于开发者。

如果用户希望以CSV文件的形式查看分析结果,则可以设置PROFILER_LOG环境变量,并将CSV文件路径设置。在主应用程序完全运行之后,分析数据将保存到指定的CSV文件中。

如前所述,分析器CSV文件包含执行的函数/方法名称、它被调用的次数、函数及其子函数总共花费的时间(以毫秒为单位)、函数本身花费的时间,即自时间,以及子函数花费的时间。

限制

/Gh和/GH编译器标志仅支持Win32平台,因此当前分析器对原生64位应用程序没有用处。

改进范围

本文讨论的简单分析器本身是完整的,但它可以进一步改进。可以想到以下改进方式,或者使其对开发者更友好:

  • 可以使用多媒体计时器代替默认时钟以获得精确的时间
  • 可以添加内存分析器
  • 可以创建Visual Studio插件或宏,以便快速进行完整解决方案的分析
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485