在过去十年间,涌现了许多新的软件技术和平台。每种新技术都要求掌握专门的知识才能创建出性能良好的应用程序。现在,由于各种 Internet 技术(如博客)使失望的用户可轻松地否定您的应用程序,因此您确实需要将性能放到首要位置。在计划早期,就应添加响应性能要求并创建原型来确定可能的技术限制。在整个开发过程中,还应衡量应用程序的各个性能方面以发现可能的性能下降,同时确保速度较慢情形下的测试人员文件并跟踪其错误。
即使拥有最好的计划,仍必须在产品开发过程中调查性能问题。在本文中,我们将向您展示如何使用 Visual Studio® Team System Development Edition 或 Visual Studio Team Suite 来确定应用程序中的性能瓶颈。将通过演练一个示例性能调查来向您介绍 Visual Studio 分析器。请注意,尽管我们在本文中是使用 C# 来编写代码示例,但是此处的大部分示例对于本机 C/C++ 和 Visual Basic® 代码也同样有效。
应用程序分析
我们将使用先前提及的两个 Visual Studio 版本所附带的分析器。首先编写一个用于绘制 Mandelbrot 不规则图形的小型示例项目(如图 1 所示)。该应用程序不是非常有效,并且需要约 10 秒钟才能绘制出不规则图形。
Figure 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% 的样本则分散在其他各个函数间,在此就不加赘述。要了解更多有关报告选项的信息,请参阅侧栏“报告可视化选项”。
Figure 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 通过其子项产生了大部分处理。它才是应用程序的真正瓶颈。
Figure 3 被测应用程序的调用树示例 (单击该图像获得较大视图)
显然地,Bitmap.SetPixel 对于 Mandel 项目而言并非最佳。我们的应用程序需要一种更快的方式来访问窗体上的所有像素。幸运的是,位图类还提供有另一个有用的 API:Bitmap.LockBits。此函数允许程序直接写入位图内存,从而减少了设置单个像素的开销。此外,为优化绘图,我们将创建一个纯整数数组并用每个像素的颜色值加以填充。随后,通过单个操作将该数组的值复制到位图中。
优化应用程序
接下来修改 DrawMandel 方法以使用 LockBits 而非 SetPixel,并看看此更改会产生何种性能。创建位图后,添加以下代码行来锁定位图位并获得指向位图内存的指针:
BitmapData bmpData = bitmap.LockBits( new Rectangle(0, 0, Width, Height), ImageLockMode.ReadWrite, bitmap.PixelFormat); IntPtr ptr = bmpData.Scan0; int pixels = bitmap.Width * bitmap.Height; Int32[] rgbValues = new Int32[pixels];
然后,在设置像素的内部循环中,注释掉对 Bitmap.SetPixel 的调用并用新语句加以替换,具体如下:
//bitmap.SetPixel(column, row, colors[color]); rgbValues[row * Width + column] = colors[color].ToArgb();
此外,添加以下代码行来将数组复制到位图内存中:
Marshal.Copy(rgbValues, 0, ptr, pixels); bitmap.UnlockBits(bmpData);
现在,如果重新在分析器中运行应用程序,可以看到不规则图形的绘制速度几乎快了三倍(请参见图 4)。请注意,新性能报告的摘要页面显示 DrawMandel 的主体直接占据了总样本的 83.66%。由于我们优化了绘图,瓶颈现在变成了不规则图形的计算。
Figure 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 循环是不变的。
Figure 6 Code-Level Optimizations
原始代码
for (int column = 1; column < Width; column++) { y = yStart; for (int row = 1; row < Height; row++) { double x1 = 0; double y1 = 0; int color = 0; int dept = 0; while (dept < 100 && Math.Sqrt((x1 * x1) + (y1 * y1)) < 2) { dept++; double temp = (x1 * x1) - (y1 * y1) + x; y1 = 2 * x1 * y1 + y; x1 = temp; double percentFactor = dept / (100.0); color = ((int)(percentFactor * 255)); } //Comment this line to avoid calling Bitmap.SetPixel: //bitmap.SetPixel(column, row, colors[color]); //Uncomment the block below to avoid Bitmap.SetPixel: rgbValues[row * Width + column] = colors[color].ToArgb(); y += deltaY; } x += deltaX; }
优化后的代码
for (int column = 1; column < this.Width; ++column) { y = yStart; int index = column; for (int row = 1; row < Height; row++) { double x1 = 0; double y1 = 0; int dept = 0; double x1Sqr, y1Sqr; while (dept < 100 && ((x1Sqr = x1 * x1) + (y1Sqr = y1 * y1)) < 4) { dept++; double temp = x1Sqr - y1Sqr + x; y1 = 2 * x1 * y1 + y; x1 = temp; } rgbValues[index] = colors[((int)(dept * 2.55))].ToArgb(); index += Width; y += deltaY; } x += deltaX; }
修改后,重新分析应用程序并检查优化后代码的性能。生成和运行应用程序后,现在在 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。超过了十倍的优化!为展示所取得的性能改进,可复制并粘贴该报告的任何部分。
Figure 7 比较 DrawMandel 的优化结果 (单击该图像获得较大视图)
目标分析
到目前为止,我们已演示了如何使用 Visual Studio 分析器来改善应用程序的性能。但是,许多实际应用程序都需要执行多个用户操作才能了解性能问题。通常,您可能宁愿忽略在开始分析前收集的所有数据。此外,可能希望在单次运行中从多种情形收集数据。
为演示如何针对此类情况使用分析器,我们将切换话题并分析一个示例电子商务网站(实际上,我们使用的是一个修改版本的 TheBeerHouse 示例,可从 asp.net/downloads/starter-kits/the-beer-house 获得该示例)。此网站的加载时间很长,但由于这是一次性开销,因此我们对启动时间的兴趣不大,而是对需要花很长时间才能加载产品目录的原因以及将项目添加到购物车非常慢的原因更感兴趣。
根据本文目的,我们将仅展示对第一种情形的调查。但是,我们将针对这两种情形收集数据,并向您展示如何筛选数据以重点关注特定情形的性能问题。
首先,创建一个新的分析会话。如前所述,通过“Analyze”(分析)菜单启动“Performance Wizard”(性能向导),然后通过向导页面选择默认值。请注意,对于网站,“Instrumentation Profiling”(检测分析)是默认选项。网站一般不受 CPU 限制,通常依靠数据库服务器应用程序来承担繁重的任务。因此,检测是更好的选项。
创建性能会话后,在分析器中启动网站,但是我们将避免启动时间并且一次仅关注一种情形。为此,在 Visual Studio 2008 的“Performance Explorer”(性能资源管理器)中,可使用“Launch with Profiling Paused”(启动并暂停分析)选项来启动附加有 Visual Studio 分析器的应用程序,但是分析器在用户恢复分析之前不会收集任何数据(请参见图 8)。
Figure 8 启动时暂停分析器
当网站正在加载时,切换回 Visual Studio。请注意,Visual Studio 分析器显示一个名为“Data Collection Control”(数据收集控制)的新工具窗口。此窗口允许您多次暂停和恢复收集。该控制的一个重要部分是预定义标记列表。它们是可插入分析数据来指出感兴趣的时间点的书签或标签。我们使用这些标记来分隔每个用户情形的开头和结尾。
首先,使用上下文菜单中的“Rename Mark”(重命名标记)命令来重命名四个标记。并删除未使用的标记(请参见图 9)。到目前为止,我们已暂停分析以避免启动时间收集并准备我们的情形。网站加载完毕后,恢复分析。
Figure 9 为测试情形命名分析标记
我们已做好开始第一个情形的准备。通过选择 Product Catalog Request 标记并单击“Insert Mark”(插入标记)按钮,标记该情形的开头。然后切换回 Internet Explorer®,通过显示产品目录完成第一个情形。网站显示产品目录后,插入 Product Catalog Rendered 标记来指明此情形的结尾。要转换到下一个情形,选择 Beer cap 产品。再次在添加前后插入各自的标记。此时即完成所有情形,然后退出应用程序。
完成数据分析后,Visual Studio 分析器显示“Performance Report Summary”(性能报告摘要)。此报告与采样报告稍有不同,因为它显示的是调用次数最多的函数以及时间最长函数的持续时间。值得注意的是,此数据是在整个应用程序的生存期聚合的,并且包括以上两个情形和所有之前的活动。
显然,我们希望性能报告只向我们显示给定情形的数据,并且筛选出其余部分。在 Visual Studio 2008 中,分析器具有一个新的“Marks”(标记)视图可列出所有插入的标记。(请注意,Visual Studio 分析器将其他自动标记插入程序的开头和结尾)。要为第一个情形创建筛选器,选择表明该情形开头和结尾的标记,然后在上下文菜单中选择“Add Filter on Marks”(针对标记添加筛选器)。从而自动创建所需的筛选器(请参见图 10)。除标记外,也可按线程、进程或时间间隔进行筛选。设置筛选器后,可继续执行它。
Figure 10 性能测试的目标程序 (单击该图像获得较大视图)
请注意,此筛选适用于性能报告中的所有视图。这也是 Visual Studio 分析器自动显示已筛选数据的新摘要页面的原因。此摘要页面特定于产品目录呈现情形。例如,可以看到 System.IDisposable.Dispose 花了 3.4 秒或 61% 的情形执行时间,而之前需要 41%。通过筛选,我们可以准确地看出此函数对于特定问题的重要程度。
现在修复此性能问题。需在代码中找到处理这些对象的函数。最简单的方法是使用“调用树”和“热路径”功能(如图 11 所示)。它会立即显示是 SetInputControlsHighlight 函数引起对 IDisposable.Dispose 的大多数调用。
Figure 11 使用热路径查找问题 (单击该图像获得较大视图)
事实证明该函数包含一个效率很低的日志记录机制:
foreach (Control ctl in container.Controls) { log += "Setting up control: " + ctl.ClientID; string tempDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); using (StreamWriter sw = new StreamWriter( Path.Combine(tempDir, "WebSite.log"), true)) { sw.WriteLine(log); } ...
它是在调试过程中错误留下的大量日志并且不再用于任何特定诊断目的,因此可以放心地将其删除。同样,Visual Studio 2008 中的“热路径”功能可让我们快速确定应用程序中的瓶颈。
无论您是用本机 C/C++、C# 还是 Visual Basic 来编写应用程序,Visual Studio 分析器都能显著简化性能调查,并帮助您编写更快更有效的应用程序。Visual Studio 2008 为 Visual Studio 分析器带来了更多改进功能,因而可比以往更轻松地找出应用程序中的性能瓶颈。