VS程序性能分析器 -- 使用说明

Visual Studio 2005提供了一个方便易用的程序性能分析器,从“工具”菜单中选择“性能工具”子菜单,即可启动一个“性能向导”,通过此向导可完成对程序分析器的设置工作。

本节通过一个实例介绍如何使用Visual Studio 2005提供的程序性能分析器。

10.2.1  读懂示例程序代码

首先请读者运行一下配套光盘中本章的示例项目PerformanceTest,程序运行界面如图10-1所示。

图10-1  示例项目PerformanceTest

示例项目完成的功能是给文本加上行号。请准备一个拥有数百行的长文本作为测试文本,并事先复制到剪贴板上。

如图10-1所示,单击“粘贴”按钮将准备好的长文本粘贴到文本框中,用鼠标选择文本,或单击“全选”按钮选中全部文本,再单击“插入行号”按钮即可给选中的文本插入行号。

示例项目PerformanceText有两个版本,请先运行一下原始版本,再运行一下性能优化后的版本,就可以直观地感受到程序性能的提高。

程序中使用RichTextBox控件,插入行号时保持原文本的格式。

下面介绍一下RichTextBox控件的基础知识,了解这些知识有助于读者读懂示例代码。

RichTextBox控件接收一种RTF格式的文本,其文字可以拥有字体、字号、颜色等特性,类似于Word。RichTextBox控件的Text属性代表其内部的所有文本,RTF属性代表Text属性对应的RTF格式的数据。

RichTextBox内部将文本看成是行的集合,其Lines属性是一个字串数组,通过在Lines数组中指定一个索引,即可访问或修改特定行。

RichTextBox另一个重要的属性是CharIndex,它代表从文档开头到某个字符的索引,即文档中的某个字符“到文档开头有多远”。

如图10-2所示为CharIndex和Lines属性的示意图。

图10-2  RichTextBox的两个属性示意图

请注意区分Lines数组中的索引与CharIndex属性。在示例代码中将使用这两个属性来完成插入行号的工作。

现在分析一下原始版示例程序的代码。

//获取每行第一个字符的索引

//lineIndex为整个文档中每行的行号,从0开始

private int GetFirstCharIndexFromLine(int lineIndex)

{

    //第一行第一个字符肯定是0

    if (lineIndex == 0)

        return 0;

    //以下计算行号大于0的行的首字符索引

    int charIndex = 0;

    //累加前面的整行

    for (int i = 0; i < lineIndex; i++)

        //每行最后要多加一个字符的长度

        charIndex += this.richTextBox1.Lines[i].Length + 1;

    return charIndex;

}

以下代码完成插入行号的功能。

//插入行号

private void InsertNumber()

{

    richTextBox1.WordWrap = false;

    // 开始行,结束行

    int beginLine =

        richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart);

    int endLine =

        richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart +

        richTextBox1.SelectionLength - 1);

    //获取选中的总行数

    int totalLines = endLine - beginLine + 1;

    for (int i = 0; i < totalLines; i++)

    {

        //将插入点光标移到每行开头

        richTextBox1.SelectionStart = GetFirstCharIndexFromLine(

                                        i + beginLine);

        richTextBox1.SelectionLength = 0;

        richTextBox1.SelectionColor = Color.Black;

        //插入行号

        richTextBox1.SelectedText = (i + 1).ToString() + " ";

    }

}

由于必须在插入行号后保持原有文字的格式,所以,不能直接修改Lines数组,因为里面只包含了文本信息。而是采用插入文本的方法,将光标先移到每行开头再插入。

10.2.2  对示例程序进行性能分析

从“工具”菜单中选择“性能工具”à“性能向导”命令,打开“性能向导”的第1页(见图10-3)。(如果是Vista系统, VS需要用管理员权限启动)

图10-3  性能向导(1)

如图10-3所示,在下拉列表框中选择分析对象。Visual Studio 2005性能分析器可以分析.EXE、.DLL和ASP.NET页面,当前解决方案中的项目会在下拉列表中出现。

在本章示例中由于只有一个Windows应用程序项目,所以只可选择PerformanceTest。

选择好分析的目标之后,单击“下一步”按钮,进入“性能向导”的第2页(见图10-4)。

图10-4  性能向导(2)

性能向导的第2页要求指定分析方法,Visual Studio 2005支持两种分析方法,即采样与检测。

采样是定期中断程序的执行,记录并分析其函数调用情况。比较适合于找出整个程序中的性能瓶颈。

检测可提供更多信息,主要用于比对特定函数代码优化前后的程序性能。

一般来说,第一次运行性能分析器时建议选取“采样”分析方法,以确定好要分析的函数。以后则采用“检测”分析方法以开始优化代码。

选择好分析方法后,单击“下一步”按钮显示“性能向导”第3页(见图10-5)。

图10-5  性能向导(3)

运行性能向导结束后,一个性能资源管理器面板出现在屏幕上(见图10‑6)。

在“性能资源管理器”中单击“启动” 按钮即可启动程序,性能分析器自动在后台监控程序运行。

这时,可以按照一般的习惯操作程序,当程序退出时,性能分析器会自动整理收集到的信息并生成一个报告。

现在,请读者通过“性能资源管理器”启动程序,并向其中粘贴一个较长的文本(我的示例有140行),全选,然后单击“插入行号”按钮,耐心等待插入工作完成后,结束整个程序的运行。则性能分析器会自动生成一个报告文件,如图10-7所示。

             

图10-6  性能资源管理器                           图10-7  自动生成性能分析报告

双击图10-7中所示的报告节点PerformanceTest061217.vsp,Visual Studio 2005会在主工作区中显示此报告的内容(见图10-8)。

图10-8  性能分析报告

如图10-8所示为采用“采样”方法分析的结果,可以看到其中列出了执行时间最长的采样函数列表。

大多数情况下我们不关心CLR的运行情况(如图10-8中所示的mscorwks.dll与mscoree.dll),也不关心系统提供给我们的“官方控件”的运行情况(如图10-8中所示的RichEd20.DLL),因为这些组件都不是我们所能控制的。我们只关心自己代码的运行情况。

为此,在图10-8中单击“函数”按钮,转入“函数”视图(见图10-9)。

图10-9  性能分析报告的“函数”视图

在图10-9中找到“PerformanceTest.exe”节点并展开,点击“Inclusive Percent”列进行降序排列。

“Inclusive Percent”数值反映了某函数及其调用的子函数占用取样样本的比例,数值越大表明其占用CPU时间越多,因而对程序性能影响越大。

在图10-9中可以清晰地看到,Main()函数高居榜首,这是肯定的,因为它是程序入口点,自然执行时间最长。

紧随其后的有3个函数:btnInsertLine_Click()、InsertNumber()和GetFirstCharIndex FromLine(),都占用了大量的CPU时间。

现在我们来看看这些函数的调用情况。

在图10-9中的“InsertNumber”节点上单击鼠标右键,从弹出菜单中选择“显示调用InsertNumber的函数”命令,将会自动转到“调用方/被调用方”视图(见图10-10)。

图10-10 “调用方/被调用方”视图

从图10-10中可以很清禁地看到,btnInsertLine_Click()函数调用InsertNumber()函数,占用的样本数均为89.648,这说明这两个函数“速度几乎是一样快的”。所以,InsertNumber()是影响btnInsertLine_Click()函数的主要因素。

再看下方显示的InsertNumber函数调用的子函数列表,GetFirstCharIndexFromLine()以86.766的数值高居“榜首”,这说明GetFirstCharIndexFromLine()函数是影响InsertNumber()函数运行速度的“罪魁祸首”。

根据上述分析,可以很有把握地推断,程序性能问题就出在InsertNumber()函数调用的GetFirstCharIndexFromLine()函数上。

在图10-10中的GetFirstCharIndexFromLine()节点上单击鼠标右键,从弹出菜单中选择“查看源”命令,则自动打开GetFirstCharIndexFromLine()函数的代码,如下所示。

01          private int GetFirstCharIndexFromLine(int lineIndex)

02          {

03              //……

04              //以下计算行号大于0的行的首字符索引

05              int charIndex = 0;

06              //累加前面的整行

07              for (int i = 0; i < lineIndex; i++)

08                  //每行最后要多加一个字符的长度

09                  charIndex += this.richTextBox1.Lines[i].Length + 1;

10              return charIndex;

11          }

在上面代码中可以看到第7句有一个循环。

再打开InsertNumber函数的代码,发现第8句也有一个循环语句。

01  //插入行号

02  private void InsertNumber()

03  {

04      //……

05      for (int i = 0; i < totalLines; i++)

06     {

07         //……

08         richTextBox1.SelectionStart = GetFirstCharIndexFromLine(i +

                beginLine);

09         //……

10  }

11  }

这是一个典型的“大循环套小循环”,初步估计是由于循环的次数过多影响了程序运行性能。

为了获取更多的信息,将“性能资源管理器”中的分析方法由“采样”改为“检测”(见图10-11)。

图10-11  转换分析方法

之后,再次运行性能分析会话,查看GetFirstCharIndexFromLine()函数的执行时间(见图10-12)。

图10-12  获取函数执行时间(1)

为了能方便地查看特定函数的调用次数与执行时间,我们可以隐藏图10-12中的部分列。请在“性能分析报告”的表头上单击鼠标右键,在弹出的窗口中选择希望显示的列(见图10-13)。

如图10-13所示,“Number of Calls”列显示函数的调用次数,“Elapsed Inclusive Time”列显示函数(包括它所调用的子函数)所执行的时间。

图10-13  选择要查看的列

经过调整的性能分析报告如图10-14所示。

图10-14  获取函数执行时间(2)

可以看到,GetFirstCharIndexFromLine()被调用140次,执行了3866.757339毫秒。InsertNumber()函数调用1次,执行了4301.149911毫秒。“插入行号”按钮单击事件响应代码btnInsertLine_Click()函数执行4303.952901毫秒。很明显,时间都耗在执行GetFirstCharIndexFromLine()函数上了。

因此,我们找到了程序运行缓慢的原因,主要就是被多次调用的GetFirstCharIndex FromLine()函数性能太低,此函数就是整个程序的“性能瓶颈”。

10.2.3  优化代码

在找到程序的性能瓶颈之后,即可动手优化代码。

1.初次优化——使用Win32 API消除循环

首先想到的就是GetFirstCharIndexFromLine()函数能否运行得更快?查看源代码,发现其中有循环语句,能否消除?

经过上网查询各种资料,发现.NET Framework提供的RichTextBox控件其实是由以前的ActiveX控件RichEdit包装而来,于是想到了使用Win32 API函数SendMessage()向其发送特定的消息来直接获取每行的第一个字符索引。

这时,需要使用平台调用技术,写出一个新的函数GetFirstCharIndexFromLine2()取代老的GetFirstCharIndexFromLine()函数。

//通过调用Win32 API来获取每行第一个字符的CharIndex值

[System.Runtime.InteropServices.DllImport("user32.DLL", EntryPoint =   

    "SendMessageA", SetLastError = true)]

private static extern int SendMessage  (System.IntPtr hwnd , UInt32  wMsg ,

    int wParam , int lParam );

private const uint EM_LINEINDEX = 0xBB;

//第二种方法,速度快,直接调用Win32 API获取每行的首字符索引

private int GetFirstCharIndexFromLine2(int lineIndex)

{

    return SendMessage(this.richTextBox1.Handle,EM_LINEINDEX,lineIndex,0);

}

修改InsertNumber()函数如下:

//插入行号

private void InsertNumber()

{

      for (int i = 0; i < totalLines; i++)

      {

      richTextBox1.SelectionStart = GetFirstCharIndexFromLine2(i+beginLine);

      }

}

现在开始检测代码优化后的程序性能。

从“性能资源管理器”中启动程序,执行与上一次相同的工作任务,向一个140行的文档中插入行号,会明显感到速度快了很多,其性能分析报告如图10-15所示。

图10-15  使用Win32 API后的程序性能分析报告

从图10-15中可以看到,GetFirstCharIndexFromLine2()函数现在执行了1.799031毫秒,而原先的GetFirstCharIndexFromLine()函数执行了3866.757339毫秒;相应地,InsertNumber()函数现在执行390.064911毫秒(原来为4301.149911毫秒),btnInsertLine_Click()函数现在执行392.692781毫秒(原来为4303.952901毫秒),性能的提升幅度是惊人的。

对应地,由于示例文档有140行,所以,GetFirstCharIndexFromLine2()函数与SendMessage()函数均执行了140次。调用次数还是多了些,能否进一步优化?

2.再次优化——引入局部变量以减少函数调用次数

由于可以访问RichTextBox的Lines数组,所以,可以只获取选中区域的第一行第一个字符的CharIndex值,以后只需加上本行字串的长度即可得到下一行第一个字符的CharIndex值。

修改后的代码如下:

//插入行号

private void InsertNumber()

{

    richTextBox1.WordWrap = false;

    // 开始行,结束行

    //……

    int begin =GetFirstCharIndexFromLine2(beginLine);

    for (int i = 0; i < totalLines; i++)

    {

        //将插入点光标移到每行开头

        richTextBox1.SelectionStart = begin;

        richTextBox1.SelectionLength = 0;

        richTextBox1.SelectionColor = Color.Black;

        //插入行号

        richTextBox1.SelectedText = (i + 1).ToString() + " ";

        begin += richTextBox1.Lines[i+beginLine ].Length + 1;

    }

}

再次运行,性能分析报告如图10-16所示。

图10-16  使用Lines数组减少函数调用次数后的性能分析报告

从图10-16中可以看到,GetFirstCharIndexFromLine2()函数与SendMessage()函数均执行了1次,调用次数大幅度减少。前者的执行时间为0.352875毫秒(原来为1.799031毫秒),快了5倍左右。

然而,很奇怪的是,btnInsertLine_Click()函数现在执行的时间是479.148527毫秒,而在引入局部变量begin之前,其执行时间为392.692781毫秒,以前的版本速度更快。而我们的程序速度主要就体现在btnInsertLine_Click()函数上,因为它是调用者,显然我们画蛇添足了,新的代码不如旧的好。

思索一下:

为何被调用的函数执行快了,调用次数少了,调用它的函数反而执行慢了?

对比前后所修改的地方,发现新的InsertNumber()函数就比原来的老函数多增加了一个局部变量begin,并多加了一句代码:

begin += richTextBox1.Lines[i+beginLine].Length + 1;

问题一定就在这里。因为修改前后两个代码只有这些不同。

提示:读者现在一定体会到“步步为营”的重要性了,在修改代码时,一次只改动一点,然后马上检测这些改动的效果如何,这样即使出错,也能很快地定位错误。

Visual Studio 2005性能分析工具还提供了更多的手段来分析程序性能问题。

在“性能资源管理器”的“性能对话”节点PerformanceTest上单击鼠标右键,从弹出菜单中选择“属性”命令(见图10-17)。

图10-17  设定性能会话的属性

在图10-17中,选中“收集.NET对象分配信息”复选框,然后再次对比InsertNumber()函数的两种代码(一种是直接调用140次GetFirstCharIndexFromLine2()函数的版本,另一种是引入一个局部变量begin后的版本),其结果如图10-18所示。

图10-18  未引入局部变量的内存分配情况

请注意未引入局部变量begin时,btnInsertLine_Click()函数占1604274字节的空间。再看看引入了局部变量后的内存占用情况(见图10-19)。

图10-19  引入局部变量后的内存分配情况

引入局部变量begin后,btnInsertLine_Click()函数占用13328198字节的空间,是原来的8倍多。

内存占用量的增大和函数执行速度的减慢,估计是受以下这句代码的影响。

begin += richTextBox1.Lines[i + beginLine].Length + 1;

对此现象的分析涉及到RichTextBox控件内部的实现代码,C#编译器生成的IL指令以及CLR深层技术内幕,由于资料的匮乏笔者不能对此现象进行解释,但读者至少可以知道,笔者“自作聪明”地引入一个局部变量begin以减少对GetFirstCharIndexFromLine2()函数的调用次数,这种做法并不能达到让程序运行得更快的预期目的。

3.再次尝试——使用RichTextBox自身方法取代Win32API

经过前面的试验,我们已经得到了比较理想的方案,这就是使用Win32 API来编写一个GetFirstCharIndexFromLine2()函数,并且不引入新的局部变量。

但仔细查看Visual Studio 2005文档,发现.NET Framework 2.0中的RichTextBox控件比1.1版增加了一个方法,名字就叫GetFirstCharIndexFromLine(),如果用RichTextBox控件自身的方法取代自己编写的GetFirstCharIndexFromLine2()函数,效果如何呢?

请读者自行进行此试验。

我的结果是:

调用RichTextBox.GetFirstCharIndexFromLine()方法的程序运行速度与调用函数GetFirstCharIndexFromLine2()(未引入局部变量begin)相比慢一些(但仅有10多毫秒的差异),内存占用则相同。

由此我推测,RichTextBox.GetFirstCharIndexFromLine()方法的内部实现代码也是调用Win32 API函数SendMessage()实现的。

推荐使用RichTextBox自身方法来取代Win32 API,一方面代码更少而运行速度并未慢多少,而且更重要的是,这将使我们编写的代码不再依赖于特定的Windows平台API函数,而是由.NET Framework标准控件RichTextBox帮我们封装好的“纯的”.NET代码,从而提高了我们代码的可移植性。

4.小结

根据这个示例项目,可以得出以下结论。

(1)需要提高程序运行速度时,应该首先想办法确定程序的性能瓶颈所在,这时可以通过性能分析工具的“采样”方法来找出占用CPU时钟周期最多的函数,从而为找到最影响程序性能的瓶颈提供线索,这样才能“打蛇打在七寸上,一击致命”。

(2)找到了影响程序性能的主要函数之后,即可通过阅读源码对代码进行优化,利用性能分析工具的“检测”方法,比对代码修改前后的程序运行性能,从而最终确定优化方案。

进行性能分析需注意的问题:

当程序中有许多项功能时,每次只分析一个功能。而且保证每次分析时使用的数据与所进行的操作都是一样的。

本章介绍的实例是一个单独的EXE程序。Visual Studio 2005还支持直接对DLL的性能分析,这时,需要指定一个外部的EXE文件,其方法是给DLL项目创建一个性能会话,然后点击DLL性能会话节点,从弹出菜单中选择“属性”命令(见图10-20)。

图10-20  分析独立的DLL文件

图10-20所示的PublicUIComponent为一个类库项目,通过指定一个外部的调用此DLL的EXE文件,Visual Studio 2005就可以对DLL进行性能分析。

如果需要进行操作系统内核级别的分析,则可以在进行性能分析时加入操作系统提供的计数器(见图10-21)。

图10-21  选择计数器

如图10-21所示为性能会话节点的属性页,由于笔者电脑的CPU为Pentium IV,所以可以使用专为Pentium IV提供的计数器。

总之,Visual Studio 2005新引入的程序性能分析器是一个很强大的工具,掌握这一工具,有助于开发出性能优异的应用程序。但前提还是得要求程序员自身对于技术要有很好的掌握,不然,使用再强大的工具也写不出好程序来。

调用DLL中非托管函数的一般方法

首先,应该在C#语言源程序中声明外部方法,其基本形式是:

[DLLImport(“DLL文件”)]

修饰符 extern 返回变量类型 方法名称 (参数列表)

其中:

  ● DLL文件:指被调用的非托管函数所在的DLL文件名。

q 修饰符:访问修饰符,除了abstract以外,在声明方法时可以使用的修饰符,如public、private。

q 返回变量类型:在DLL文件中需调用方法的返回变量类型。

q 方法名称:在DLL文件中需调用方法的名字。

q 参数列表:列出在DLL文件中需调用方法的参数,可查询表10-1将C/C++的数据类型转换为.NET的数据类型。

表10-1  非托管函数中的数据类型与.NET的数据类型对照表

 

Wtypes.h中的非托管类型                     非托管C语言类型                .NET Framework中对应的托管类名
 
HANDLE                                             void*                              System.IntPtr
 
BYTE                                                 unsigned char                  System.Byte
 
SHORT                                              short                                System.Int16 

WORD                                              unsigned short                 System.UInt16
 
INT                                                    int                                    System.Int32
 
UINT                                                  unsigned int                    System.UInt32
 
LONG                                                 long                                 System.Int32
 
BOOL                                                 long                                 System.Int32
 
DWORD                                              unsigned long                 System.UInt32
 
ULONG                                               unsigned long                  System.UInt32
 
CHAR                                                  char                                 System.Char
 
LPSTR                                                 char*                               System.String或System.Text.StringBuilder
 
LPCSTR                                               Const char*                     System.String或System.Text.StringBuilder
 
LPWSTR                                               wchar_t*                         System.String或System.Text.StringBuilder
 
LPCWSTR                                             Const wchar_t*               System.String或System.Text.StringBuilder
 
FLOAT                                                   Float                               System.Single
 
DOUBLE                                                Double                             System.Double
 

注意:

(1)需要在程序声明中使用System.Runtime.InteropServices命名空间。

(2)DllImport只能放置在方法声明上。

(3)DLL文件必须位于程序当前目录或系统定义的查询路径中(即:系统环境变量中Path所设置的路径)。

(4)返回的变量类型、方法名称、参数列表一定要与DLL文件中的原始定义相一致。

(5)若要使用其他函数名,则可以使用EntryPoint属性设置。例如:

[DllImport("user32.dll", EntryPoint="MessageBoxA")]

static extern int MsgBox(int hWnd, string msg, string caption, int type);

事实上,在.NET中调用非托管函数还有许多复杂的问题,更详细的技术细节请在Visual Studio 2005文档中查询“平台调用”。

 

 

转载自:http://blog.csdn.net/agan4014/archive/2008/03/20/2199522.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值