65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Visual Studio C/C++ 编译器和 DIA SDK 的简单性能分析器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (36投票s)

2009年12月15日

CPOL

6分钟阅读

viewsIcon

145903

downloadIcon

3315

一个易于使用的性能分析器,用于对使用 Visual Studio C/C++ 编译器(/Gh 和 /GH 标志)和 DIA SDK 收集性能数据的 C/C++ 代码进行时间与影响分析。

引言

作为一名 C++ 程序员,我总是会编写一些作为主应用程序的附加组件或第三方 DLL 的代码。因此,作为一名开发人员,我必须确保我的代码不会降低主应用程序或产品的性能。通常,我使用 IBM Rational Product Suite 或 Bounds Checker 等商业产品。但有时,这些商业工具并不好用,因为我编写的代码(DLL)依赖于主应用程序,而主应用程序根本无法修改来进行性能分析。这里的主要目的是要了解我的代码中每个函数花费的时间以及该函数被调用的次数。简而言之,我想对我的代码进行影响分析,以便为性能改进提供输入。因此,我可以确保主应用程序或产品的整体性能得到维持。

背景

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

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
    }
}

实现中有趣的部分是如何调用一个非裸(naked)的全局函数,即 enterFunc,并将其调用者函数的虚拟地址作为参数。在编写了 Prolog 指令后,通过从当前堆栈指针添加 32 字节来遍历堆栈。然后,通过指针操作从该虚拟地址获取返回地址。现在,从该地址减去 5 个字节,这将获得调用者函数体内的虚拟地址。此虚拟地址将作为参数传递给执行进一步处理的全局函数。这样就解决了计时问题。但是,函数名称呢?我应该如何从任何函数体内的虚拟地址获取函数名称?

我通过使用 DIADebug Interface AccessSDK 解决了名称问题。DIA SDK 有一个统一的模型,可以从 PDB 文件访问或查询任何符号及其属性。因此,为了使用此性能分析器,DLL 或 EXE 必须具有调试信息(PDB 文件),这是重要且强制性的。

我在 Profiler 类中实现了 getFunc,用于从给定的虚拟地址找出函数名称。过程如下:

  • 使用 GetCurrentProcess 函数获取当前进程句柄
  • 使用 EnumProcessModules 函数获取当前进程的所有已加载模块
  • 使用模块的地址空间大小和加载地址检查给定的虚拟地址是否属于任何模块
  • 如果给定的虚拟地址属于该模块,则使用 GetModuleFileNameEx 从模块句柄获取模块文件路径
  • 使用 IDiaDataSourceloadDataForExe 方法从模块文件路径加载 PDB 文件
  • 使用 IDiaDataSourceopenSession 方法获取 IDiaSession,并使用 IDiaSessionput_loadAddress 方法为查询设置符号数据库
  • 现在,使用 findSymbolByVA 方法查询 IDiaSession 对象,该方法将返回 IDiaSymbol,即包含给定虚拟地址的函数
  • 使用 get_name 方法从 IDiaSymbol 对象获取函数名称

Using the Code

本文介绍的性能分析器使用 DIA SDK,因此,如果您想使用此性能分析器,被分析的项目(LIB/DLL/EXE)必须生成调试信息。

要为项目生成调试信息,请转到 **C/C++** 选项卡下的相应项目 **常规** 属性页,并将 **调试信息格式** 设置为 **程序数据库(/Zi)**

同样,对于 **链接器** 选项卡下的 **调试** 属性页,将 **生成调试信息** 设置为 **是(/Debug)**。这两个设置将确保为要分析的项目创建 PDB 文件。

在设置项目以生成 PDB 文件后,在 **C/C++** 选项卡下的 **命令行** 属性页的 **附加选项** 中设置 /Gh/GH 标志,如下所示。

在 **链接器** 选项卡下的 **输入** 属性页的 **附加依赖项** 中添加 profiler.lib(来自性能分析器项目的导出库)。这是一个重要的设置,因为 profiler.lib/dll 包含 _penter_pexit 的实现,性能分析器才能正常工作。

在 **链接器** 属性页的 **附加库目录** 中提供 profiler.lib 的路径。此设置取决于开发人员。

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

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

局限性

/Gh 和 /GH 编译器标志仅在 Win32 平台上受支持,因此当前的性能分析器对原生 64 位应用程序无效。

改进空间

本文讨论的简单性能分析器本身是完整的,但可以进一步改进。我想到了以下改进方法或使其对开发人员更友好:

  • 可以使用多媒体计时器而不是默认时钟来获得更精确的时间
  • 可以添加一个内存性能分析器
  • 可以创建一个 Visual Studio 插件或宏,用于对整个解决方案进行快速且持续的性能分析

参考文献

以下是我在编写本文时参考的帮助文档列表:

历史

  • 2009 年 12 月 15 日 - 文章首次发布到 The Code Project。
© . All rights reserved.