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

仅需 20 分钟,性能提升 10%

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2009年9月10日

CPOL

15分钟阅读

viewsIcon

23300

Damon Armstrong曾以为他的加密库是完美的,直到他使用ANTS Performance Profiler进行了测试。在20分钟内,他为代码带来了10%的性能提升。他是如何做到的?阅读完整的故事。

虽然可能有些谦虚的言辞掩盖了事实,但大多数专业开发者内心深处都认为,在“一到十”的评分标准上,自己是相当出色的。为什么他们会这么想呢?开发者在职业生涯中通常有两个发展方向。一个方向是通往对语法能力痛苦的认识,在那里每一天都充满了被发现自己能力不足的恐惧。自然,这会导致他们转而从事销售或市场营销。另一个方向则是征服项目,这会 boost 你的信心和自尊,直到你坚信自己的技能介于“史诗级”和“不朽级”之间。

但是,如果你的代码被赤裸裸地暴露出来,被层层剖析,被分析到极致,那种“不朽级”的技能感还会保留吗?这绝对是一个值得提出的问题,而Red Gate Software的ANTS Performance Profiler可以帮助你找到答案。不久前,我编写了一个加密库来简化加密和哈希操作。我认为这是扎实的代码,并且在许多项目中都使用过它,没有任何明显的性能问题。然而,在接下来的章节中,我将通过ANTS Performance Profiler来运行它,看看它是否能发现我“史诗级”的“不朽级”才能所忽略的任何东西。

安装ANTS Performance Profiler

为了尽可能全面地介绍ANTS Performance Profiler,我将从我的安装体验开始进行深入的概述。我从Red Gate网站下载了该软件的一个副本。然后我进行了安装。无需准备。无需配置。无需沮丧。如果你鼠标操作相对准确,并且能够点击四次“下一步”,那么你的体验应该差不多。为什么是四次?嗯,有一个你必须接受的最终用户许可协议,并且你可以选择更改安装目录。

设置应用程序进行性能分析

当你启动ANTS Performance Profiler时,你会被带到“应用程序设置屏幕”(见图01)。总的来说,这是一个相当容易理解的屏幕。首先,你选择要分析哪种类型的应用程序,然后在屏幕底部输入一些配置设置。当然,配置设置对于每种应用程序类型都不同;对于桌面应用程序,你可以指向一个可执行文件,而Web应用程序则需要你输入应用程序的URL。请注意,我确实说了**应用程序**。ANTS Performance Profiler分析的是正在运行的代码,这意味着你需要指向它能运行的东西。在我的情况下,我想分析我的加密库。幸运的是,加密库附带了一个桌面应用程序,允许你设置配置信息,加密和解密字符串,以及执行哈希操作,所以我们可以针对该应用程序运行它。

http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi1.jpg

图01 – 应用程序设置

无论应用程序类型如何,你都可以选择一个**性能分析模式**和分析器的**默认计时显示**。**性能分析模式**包含一个下拉列表,允许你指定对代码进行分析的详细程度。基本上,你可以选择是进行**逐行计时**还是**逐方法计时**,以及是否包含所有代码(例如.NET框架和第三方代码),还是只包含有源代码的代码(例如你自己的代码)。逐行计时是最详细的,意味着统计信息是针对方法中的每一行代码收集的。逐方法计时速度更快,因为统计信息是按方法而不是按行收集的。你可能会想,为什么不总是选择**逐行计时**,因为它提供了最多的细节?问得好。答案是,当你使用该应用程序收集信息时,你可能会注意到在性能分析会话中使用应用程序时会有些迟缓。如果你正在对大型应用程序进行性能测试,那么从**逐方法计时**开始,找出性能问题的相关方法,然后对这些方法进行**逐行计时**以更清楚地了解哪些行导致了问题,可能会让你受益。

**默认计时显示**下拉列表提供了两种捕获计时信息的方法:**CPU**和**挂钟时间**。当你选择CPU时间时,“方法或代码行运行的时间”是根据实际CPU使用率计算的。如果一个线程被阻塞,这意味着等待线程继续执行的时间不计入计时统计。另一方面,**挂钟时间**则计算方法和代码行执行的实际总时长:如果你有一个遇到问题的多线程应用程序,这是个不错的选择。

ANTS Performance Profiler的另一个很酷的功能是集成的性能计数器(见图02)。在分析应用程序之前,你可以选择任意数量的性能统计信息进行捕获,例如CPU使用率、内存使用率、线程数、抛出的异常、提供的页面数等。在性能分析会话期间,这些性能统计信息会与代码统计信息一起存储,并在时间线上进行图表绘制。在分析阶段,这允许你选择时间线上的一个区域,并查看在峰值、低谷或图表上引起你注意的任何东西时,正在运行什么代码。我对这个功能印象深刻,因为它真的能让你轻松地识别和隔离高度特定的性能问题。

http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi2.jpg

图02 – 性能计数器

分析应用程序

一旦你设置好配置选项,要开始分析应用程序,只需点击**开始分析**按钮即可。ANTS Performance Profiler会启动你的应用程序,然后由你来运行应用程序的任何需要分析的部分。如果你不运行它,它就不会被分析。随着你的应用程序运行,ANTS会不断收集关于哪些代码行正在运行以及它们花费了多长时间的统计信息,以及你选择的任何性能计数器数据。在我的例子中,我想测试库中的加密过程,所以我只需在UI中输入一个字符串并加密10次。完成后,点击**停止分析**以完成数据收集过程,然后进入分析阶段。

分析分析器数据

现在到了有趣的部分,查看性能结果并找出代码中的瓶颈。如果你看一下图03,你会看到我在分析我的应用程序后获得的结果。在完成分析过程后,ANTS Performance Profiler会显示几个不同的信息,下面将进行更详细的描述。

http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi3.jpg

**性能计数器列表** – 列出配置性能分析会话时选择的各种性能计数器。点击其中一个性能计数器会选中列表中的项,并在性能图上将性能计数器的线条加粗。

**性能图** – 以可视化的时间线呈现性能计数器数据。你可以通过单击并拖动鼠标来选择时间线上的一个区域。选择时间线上的一个区域会将性能分析过滤到选定的区域。

**方法树** – 显示一个包含性能分析会话期间方法调用层次结构的树。 http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi4.gif  图标可以让你识别代码花费最多执行时间的方法调用路径,从而快速定位可能存在的性能问题。

**源代码视图** – 当你在方法树中点击一个方法时,源代码视图会显示该特定方法的源代码(如果可用)。此视图还包含有关每行代码的统计信息(如果你进行了逐行数据收集),例如代码行的命中次数、执行总时间以及平均执行时间。在源代码视图窗口的滚动条附近,有**热点指示器**,对应于ANTS Performance Profiler已识别为潜在性能问题的代码行。

现在我们对屏幕上的内容有了一定的了解,让我们谈谈它的含义。首先,看看性能图。我的分析会话持续了大约1分40秒,红线表示在此期间消耗的处理器的量。请注意,性能计数器并非只局限于你的应用程序,因此如果你在后台运行了导致处理器峰值的东西,它也会在这里显示。幸运的是,当时我的系统上只有这一项主要运行的东西,所以应该没问题。注意到处理器使用率在性能分析会话开始时有一个峰值,然后变平,然后在会话结束时再次升高。第一个峰值是应用程序启动,最后一个峰值是我在应用程序中输入“**Test**”并多次点击“**Encrypt**”时产生的。

目前方法树告诉我,我的 http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi5.gif  路径是应用程序主窗口的构造函数。我正在VPC中运行一个WPF应用程序,所以这个结果并不意外。而且我确实对此无能为力,因为启动期间没有代码运行,只是XAML被解析并构建成UI。默认情况下,ANTS Performance Profiler会使用整个性能分析会话期间收集的所有统计信息。在这种情况下,我希望关注在性能分析会话后期运行的加密例程,并忽略开始时的启动统计信息。所以我将要做的是在性能图上选择一个区域,将性能分析数据限制在我的性能分析会话后期运行的代码。选择后,它看起来是这样的。

http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi6.jpg

在选择时间线上的一个区域后,方法树会更新,并且出现一个新的 http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi5.gif  路径。如前所述,我的加密库是一个包装器,它简化了现有的.NET加密库,所以我希望新的 http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi5.gif  路径能指向一个.NET Framework函数。为什么?因为这将意味着我的包装器足够轻量级,不会造成太多开销,而所有繁重的工作都发生在.NET加密库深处,我无法进行优化(换句话说,我可以责怪微软速度慢)。不幸的是,快速查看显示路径直接穿过 http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi5.gifRebel.Cryptography.CryptoSettings.LoadDefaults 方法,这有点奇怪,因为这 supposed to 只是加载一些标准的默认设置。点击方法树中的那个方法会显示下面的源代码视图。

http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi7.jpg

当你查看源代码视图时,有几列是需要了解的:

**行号** – 包含源代码的行号。如果你想知道你正在查看哪个源文件,文件名会显示在源代码视图工具窗口的标题中。

**命中次数** – 显示代码行被执行的次数。我在性能分析过程中快速连续点击了20次“Encrypt”,这就是为什么所有这些行都显示20的原因。

**平均时间(毫秒)** – 代码行运行的平均时间。这个值有几种时间单位选项——百分比、滴答、毫秒和秒。在本例中,我以毫秒为单位显示,因为它方便比较数字。你可以随时通过**时间线**菜单切换单位,所以你可以即时更改并使用最适合你当前正在分析内容的单位。

**时间(毫秒)** – 代码行运行的总时间。

现在源代码和性能统计信息摆在我们面前,是时候玩一个叫做“找出糟糕的代码行”的游戏了。有七行代码带有性能统计信息。其中两行是花括号,每次遇到它们都会消耗0.017毫秒的处理时间。我真的不知道如何优化花括号的性能,所以我们暂时忽略它们。另外五行代码基本上都在做同样的事情,设置默认属性值。但粗略一看,你会发现方法中的最后一行代码(第278行)花费了近1000毫秒执行,而其他所有代码花费的时间要么是14毫秒,要么是2毫秒。我知道第273行的setter,它花费14毫秒运行,包含额外的逻辑来清除其他一些依赖属性,这就是为什么它比其他默认setter运行时间更长。换句话说,它需要更长的时间是有道理的。但第278行花费的955毫秒似乎有点奇怪。

现在我已经确定了性能不佳的代码行,是时候找出它为什么性能不佳了。这一行正在发生两件事。首先,系统通过调用System.NET.Dns.GetHostName来获取系统名称,然后将返回的字符串值赋给Password属性。这两者的总和显示在这行代码的统计信息中,但我需要知道哪一个才是真正消耗大部分处理器时间的罪魁祸首。如何做到?很简单,当你将鼠标悬停在单行代码的每个单独语句上时,会出现一个提示,显示该单个语句的详细信息。当我悬停在password上时,它显示处理需要185毫秒。我恰好知道Password属性的setter包含大量代码,用于根据设置的密码设置初始化向量和加密密钥,所以它的值比其他setter高是有道理的。当我悬停在**GetHostName**方法上时,它显示此调用消耗了750毫秒的处理时间。ouch。

我也可以使用调用图来找出单个计时信息,调用图可以构建代码的可视化表示。如果你查看CryptoSettings.LoadDefault的调用图(你可以通过右键单击方法树中的方法并选择“创建新的调用图”来查看),你会看到Password属性的setter花费了185.506毫秒,占LoadDefault运行总时间的19%。如果你将鼠标悬停在Password setter左侧的小黄框上,你会看到对GetHostName方法的调用,右侧的小黄框代表.NET JIT开销。.NET JIT开销通常可以忽略不计,所以即使你无法直接看到统计信息,你也可以得出结论,对GetHostName的调用占用了该方法调用的其余80%。

http://www.simple-talk.com/iwritefor/articlefiles/756-TyRevi8.jpg

解决问题

System.NET.Dns.GetHostName是Microsoft的方法,这意味着我无法优化其性能。虽然我最初的目标是将最长的路径追溯到Microsoft的方法,但这并不是我真正想要的。事实上,当我第一次看到我调用**GetHostName**方法时,我脑子里第一个想到的就是……WTF?这是一个与网络无关的加密库,主机名有什么关系?然后我才想起我为什么这么做。我的加密库从应用程序的配置文件中读取设置信息,其中一项是用于填充.NET加密操作所使用的初始化向量和加密密钥的密码。然而,在我编写库的时候,我记得我不想强迫人们在使用该库时进行配置。例如,如果有人只是匆忙搭建一个演示站点需要该库,我不想让他们在配置信息中查找才能使用它。所以我需要一个“标准”密码,在没有配置可用时使用。但是硬编码密码的想法让我很不舒服,所以我决定只使用系统的hostname,这就是代码中调用GetHostName的原因。这样它就会因系统而异,但如果有人回来问,“嘿,加密密码是什么?”,仍然很容易弄清楚。

当然,我当然没有意识到调用GetHostName会产生如此大的性能影响。现在,你可能会说750毫秒在性能方面不是什么大问题,但当你将时间单位从毫秒切换回百分比时,你会很快发现750毫秒大约占我选择的时间线区域总处理时间的10%。换句话说,移除这个调用并将其替换为硬编码值,或者在找不到配置时抛出异常,可以将性能提高约10%。

最后的想法

除了写下我的经历,分析我的代码并找到一种将我的加密库性能提升10%的方法,总共花费了大约二十分钟。其中八分半钟花在了观看ANTS Performance Profiler的视频概述上。尽管ANTS Performance Profiler发现了我代码中的一些问题,这可能伤了我的自尊,但事实是我的代码现在更好,这让我更加自豪。

© . All rights reserved.