首页 > 代码库 > 使用 Visual Studio 分析器找出应用程序瓶颈(转)

使用 Visual Studio 分析器找出应用程序瓶颈(转)

使用 Visual Studio 分析器找出应用程序瓶颈

Hari Pulapaka and Boris Vidolov

本文讨论:

以性能瓶颈为目标

应用程序代码分析

比较分析数据

性能报告

本文使用了以下技术: 

Visual Studio 2008

在过去十年间,涌现了许多新的软件技术和平台。每种新技术都要求掌握专门的知识才能创建出性能良好的应用程序。现在,由于各种 Internet 技术(如博客)使失望的用户可轻松地否定您的应用程序,因此您确实需要将性能放到首要位置。在计划早期,就应添加响应性能要求并创建原型来确定可能的技术限制。在整个开发过程中,还应衡量应用程序的各个性能方面以发现可能的性能下降,同时确保速度较慢情形下的测试人员文件并跟踪其错误。

即使拥有最好的计划,仍必须在产品开发过程中调查性能问题。在本文中,我们将向您展示如何使用 Visual Studio? Team System Development Edition 或 VisualStudio Team Suite 来确定应用程序中的性能瓶颈。将通过演练一个示例性能调查来向您介绍 Visual Studio 分析器。请注意,尽管我们在本文中是使用 C# 来编写代码示例,但是此处的大部分示例对于本机 C/C++ 和 Visual Basic? 代码也同样有效。

 

应用程序分析

我们将使用先前提及的两个 Visual Studio 版本所附带的分析器。首先编写一个用于绘制 Mandelbrot 不规则图形的小型示例项目(如图 1 所示)。该应用程序不是非常有效,并且需要约 10 秒钟才能绘制出不规则图形。

图-1 性能测试的目标程序

 

要开始调查,从 Visual Studio 2008 的新“Analyze”(分析)菜单启动“Performance Wizard”(性能向导)。在 Visual Studio 2005 中,此功能可从“工具”|“性能工具”菜单获得。从而启动一个包含三个步骤的向导,其中第一步是指定目标项目或网站。第二步提供两种不同的分析方法:采样和检测。(有关这些分析方法的详细信息,请参阅“性能分析解释”侧栏。)现在,我们将选取默认值。

向导完成后,显示一个“Performance Explorer”(性能资源管理器)对话框并创建一个新的性能会话。此会话包含目标应用程序(在我们的示例中为 Mandel)并且没有报告。要启动分析,单击工具窗口工具栏中的“Launch with Profiling”(启动并分析)按钮。

应用程序绘制完不规则图形后,立即关闭窗体停止分析。Visual Studio 自动将一个新创建的报告添加到性能会话中并开始进行分析。分析完成后,Visual Studio 分析器会显示“Performance Report Summary”(性能报告摘要),列出开销最大的函数(请参见 图 2)。报告以两种方式显示这些函数。第一种方式衡量所列出函数直接或间接执行的工作。对于每个函数,数字代表在函数主体及其所有子调用中收集的积累样本。第二个列表不计算在子调用中收集的样本。此摘要页面显示 Visual Studio 分析器在执行 DrawMandel 方法期间收集了 30.71%的样本。剩余 69%的样本则分散在其他各个函数间,在此就不加赘述。要了解更多有关报告选项的信息,请参阅侧栏“报告可视化选项”。

 

图-2 性能测试显示开销较大的函数调用

请查看报告的“Call Tree”(调用树)视图(如图 3 所示),“Inclusive Samples %”(包含样本 %)列代表在函数及其子项中收集的样本。“Exclusive Samples %”(独占样本 %)列代表仅在函数主体中收集的样本。可看到 DrawMandel 方法直接调用 Bitmap.SetPixel。尽管 DrawMandel 自身占据了总样本的 30.71%,但 Visual Studio 分析器从 Bitmap.SetPixel 及其子项收集了 64.54%的样本。其中 Bitmap.SetPixel 主体仅占 0.68%(因此它并未显示在摘要页面上)。但是,Bitmap.SetPixel 通过其子项产生了大部分处理。它才是应用程序的真正瓶颈。

 

图-3 被测应用程序的调用树示例

显然地,Bitmap.SetPixel 对于 Mandel 项目而言并非最佳。我们的应用程序需要一种更快的方式来访问窗体上的所有像素。幸运的是,位图类还提供有另一个有用的 API:Bitmap.LockBits。此函数允许程序直接写入位图内存,从而减少了设置单个像素的开销。此外,为优化绘图,我们将创建一个纯整数数组并用每个像素的颜色值加以填充。随后,通过单个操作将该数组的值复制到位图中。

 

优化应用程序

性能分析解释

使用采样方法 进行分析时,分析器以一种类似于调试程序的方式附加到正在运行的进程。然后,分析器会定期中断进程并检查哪个函数处于堆栈顶部以及该函数的代码路径。换句话说,Visual Studio 分析器收集当前进程状态的样本。采样是一种非入侵式统计型分析方法。在函数中收集的样本越多,函数可能执行的处理就越多。

Visual Studio 分析器还会收集有关导致此执行的调用路径的信息。因此,此工具可在分析收集的数据后显示整个调用堆栈。默认情况下,Visual Studio 分析器每 1 千万个 CPU 周期收集一个样本。除 CPU 周期外,还可能在出现其他事件(如页面错误、系统调用、CPU 高速缓存缺失等等)时执行采样。分析会话的属性控制分析器的取样对象以及频率。

作为一个低开销解决方案,采样常常是推荐选项。但值得注意的是,采样仅在程序有效使用 CPU 时收集信息。因此,当进程在等待磁盘、网络或任意其他资源时,Visual Studio 分析器均不会收集样本。这就是如果应用程序并未有效使用 CPU,建议使用检测分析的原因。

在检测模式中,Visual Studio 分析器通过在每个函数的开头和结尾处注入特殊指令(称为探针)来修改(检测)二进制文件。探针允许分析器度量运行每个函数所花的时间。此外,分析器还会在每个外部函数调用周围添加一对探针,从而可确定这些外部调用的开销。

通过使用检测分析,可准确测量各种数据,如运行函数所花时间(“经过的时间”)、函数的调用次数以及函数正在使用 CPU(“应用程序时间”)且未被 OS 切换出来的时间。检测的缺点是收集了大量数据,因而需要花费更长的分析时间。此外,此分析模式还具有更高的运行时开销。更高开销可能不经意间更改所分析应用程序的性能特征。

通过同时使用采样和检测,还可收集基于 Microsoft? .NET Framework 的应用程序的内存分配数据。用户可使用性能会话属性页面启用和调整 .NET 内存分配数据的收集。它通常被称为内存分析,并且有大量关于该主题的 MSDN? 文档。请注意,它是分析器中唯一一个仅用于 .NET Framework 兼容代码的功能。对于其他功能,Visual Studio 分析器在本机 C/C++ 和基于 .NET 的应用程序之间完全相同。

 

接下来修改 DrawMandel 方法以使用 LockBits 而非 SetPixel,并看看此更改会产生何种性能。创建位图后,添加以下代码行来锁定位图位并获得指向位图内存的指针:

[csharp] view plaincopyprint?
 
  1. BitmapData bmpData =   
  2.     bitmap.LockBits(  
  3.         new Rectangle(0, 0, Width, Height),   
  4.         ImageLockMode.ReadWrite,   
  5.         bitmap.PixelFormat);  
  6. IntPtr ptr = bmpData.Scan0;  
  7. int pixels = bitmap.Width * bitmap.Height;  
  8. Int32[] rgbValues = new Int32[pixels];  


然后,在设置像素的内部循环中,注释掉对 Bitmap.SetPixel 的调用并用新语句加以替换,具体如下:

[csharp] view plaincopyprint?
 
  1. //bitmap.SetPixel(column, row, colors[color]);  
  2. rgbValues[row * Width + column] =   
  3.     colors[color].ToArgb();  

此外,添加以下代码行来将数组复制到位图内存中:

[csharp] view plaincopyprint?
 
  1. Marshal.Copy(rgbValues, 0, ptr, pixels);  
  2. bitmap.UnlockBits(bmpData);  

 

现在,如果重新在分析器中运行应用程序,可以看到不规则图形的绘制速度几乎快了三倍(请参见图 4)。请注意,新性能报告的摘要页面显示 DrawMandel 的主体直接占据了总样本的 83.66%。由于我们优化了绘图,瓶颈现在变成了不规则图形的计算。

 

图-4 修订代码的性能分析

现在,我们将更进一步优化该计算。遗憾的是,此次需要查找单个函数中的瓶颈。DrawMandel 是一个比较复杂的方法,因而很难知道应关注哪些计算。幸运的是,Visual Studio 2008 采样分析器还默认收集行级数据,从而有助于确定函数中的哪些行开销最大。

要查看行级数据,需从其他角度查看性能报告。从“Current View”(当前视图)菜单切换到“Modules”(模块)视图。与“Call Tree”(调用树)视图不同,“Modules”(模块)视图不会显示在父函数上下文中各函数的相互调用方式以及这些调用的开销等信息。相反,“Modules”(模块)视图包含每个可执行文件(程序集或 DLL)以及该可执行文件中每个函数的累积总样本数。Visual Studio 分析器从所有调用堆栈累积该数据。

“Modules”(模块)视图比较适合于观察更大的图片。例如,如果按“Exclusive Samples %”(独占样本 %)列排序,可以看到 Mandel.exe 自身执行 87.57%的处理。作为优化后的结果,GDI+ 占用不到 3%的处理。展开这些模块,可以看到单个方法的相同信息。此外,在 Visual Studio 2008 中,除函数级以外,还可展开树来查看单个行甚至这些行中单个指令的相同数据(请参见图 5)。

Figure 5 跳到分析的代码行

跳到源代码可查看如图 6 所示的代码。代码在最内部循环中计算平方根。此操作开销很大且占用 18% 的总应用程序处理。图 6 中突出显示的行显示了可优化的代码。第一行使用了一个不必要的平方根,而第二行对于 while 循环是不变的。

原始代码

[csharp] view plaincopyprint?
 
  1. for (int column = 1; column < Width; column++)  
  2. {  
  3.  y = yStart;  
  4.  for (int row = 1; row < Height; row++)  
  5.  {  
  6.   double x1 = 0;  
  7.   double y1 = 0;  
  8.   int color = 0;  
  9.   int dept = 0;  
  10.   while (dept < 100 && Math.Sqrt((x1 * x1) + (y1 * y1)) < 2)  
  11.   {  
  12.    dept++;  
  13.    double temp = (x1 * x1) - (y1 * y1) + x;  
  14.    y1 = 2 * x1 * y1 + y;  
  15.    x1 = temp;  
  16.    double percentFactor = dept / (100.0);  
  17.    color = ((int)(percentFactor * 255));  
  18.   }  
  19.   //Comment this line to avoid calling Bitmap.SetPixel:  
  20.   //bitmap.SetPixel(column, row, colors[color]);  
  21.   //Uncomment the block below to avoid Bitmap.SetPixel:  
  22.   rgbValues[row * Width + column] = colors[color].ToArgb();  
  23.   
  24.   y += deltaY;  
  25.  }  
  26.  x += deltaX;  
  27. }  

优化后的代码

[csharp] view plaincopyprint?
 
  1. for (int column = 1; column < this.Width; ++column)  
  2. {  
  3.  y = yStart;  
  4.  int index = column;  
  5.  for (int row = 1; row < Height; row++)  
  6.  {  
  7.   double x1 = 0;  
  8.   double y1 = 0;  
  9.   int dept = 0;  
  10.   double x1Sqr, y1Sqr;  
  11.   while (dept < 100 && ((x1Sqr = x1 * x1) + (y1Sqr = y1 * y1)) < 4)  
  12.   {  
  13.    dept++;  
  14.    double temp = x1Sqr - y1Sqr + x;  
  15.    y1 = 2 * x1 * y1 + y;  
  16.    x1 = temp;  
  17.   }  
  18.   rgbValues[index] = colors[((int)(dept * 2.55))].ToArgb();  
  19.   index += Width;  
  20.   
  21.   y += deltaY;  
  22.  }  
  23.  x += deltaX;  
  24. }    

 

 

修改后,重新分析应用程序并检查优化后代码的性能。生成和运行应用程序后,现在在 1-2 秒内就能重新绘制不规则图形。因而显著减少了应用程序的启动时间。

Visual Studio 2008 包含一个可比较两个性能报告的新功能。为实际了解此功能,我们在分析器中重新运行应用程序并捕获最新的性能报告。要查看两个应用程序版本之间的差异,在“Performance Explorer”(性能资源管理器)中选择原始报告和最新报告。右键单击报告,并单击上下文菜单中的“Compare Performance Reports”(比较性能报告)选项。此命令将生成一个新的报告,显示所有函数以及两个报告中的该函数的“Exclusive Samples %”(独占样本 %)值之间的差异。由于我们削减了总体执行时间,所以 DrawMandel 的相对百分比从 31.76 上升到 70.46。

为更好地查看实际优化效果,将比较选项窗格中的列更改为“Inclusive Samples”(包含样本)(请参见图 7)。同时将阈值增加到 1500 个样本以忽略微小的波动。此外,您可能已注意到:在默认情况下,报告显示负数或首先显示优化最少的函数(因为它常用于查找性能下降)。但是,出于优化目的,我们将反向排序 Delta 列,以便可以在顶部看到最优化的函数。请注意,DrawMandel 及其子函数的样本数从 2,064 变为 175。超过了十倍的优化!为展示所取得的性能改进,可复制并粘贴该报告的任何部分。

 

图-7 比较 DrawMandel的优化结果 

 

目标分析

报告可视化选项

Visual Studio 可 使用以下各种性能报告选项以多种方式查看性能数据:“Call Tree”(调用树)、“Modules”(模块)、“Functions”(函数)以及其他选项。打开报告时默认显示“Summary”(摘要)视图。例如,要在 Visual Studio 2008 中查找产生大多数处理的调用路径,请从“Current View”(当前视图)菜单选择“Call Tree”(调用树)视图。(在 Visual Studio 2005 中,在报告底部选择“调用树”选项卡。)“Call Tree”(调用树)视图包含所有调用堆栈的聚合树。“Inclusive Samples %”(包含样本 %)列显示这些代码路径中每个分支的总开销。沿着开销最大的分支,就可以找到性能瓶颈。

在 Visual Studio 2008 中,分析器团队添加了两个新的功能来简化性能报告的使用。添加的第一个功能是降噪选项。默认情况下,报告现在会裁掉不重要的小函数,从而使用户能很容易地查看具有更大影响的函数。此选项通常称为剪裁。另外,团队通过将自身不进行任何处理而只调用其他函数进行处理的函数放到一起,从而减少了调用树的深度。Visual Studio 分析器将其称为折叠。

性能报告中的降噪选项控制剪裁和折叠的阈值。如果在性能报告中查找特定函数时遇到问题,则可关闭降噪选项。

对 Visual Studio 2008 中调用树的第二个较大的改进是“Hot Path”(热路径)按钮和相应上下文菜单。“热路径”会突出显示程序中开销最大的代码路径,并沿着该路径向下直至看到单个函数所执行(并且不是委派)的重大处理。然后,“热路径”会突出显示该函数。如果有两个或多个单独的重要代码路径,则“热路径”将停在树中出现分支的位置。如果“热路径”为应用程序提供了多个分支,则可选择最感兴趣的一个,并对该特定分支重新应用“热路径”。