代码分析器和优化






2.74/5 (16投票s)
2002年3月28日
10分钟阅读

226983

1615
C++ 代码分析器和小型分析实用程序
引言
时下台式机CPU的运行速度高达2.2 GHz,足以推算出惊人的像素数量,因此代码分析或调优的神秘艺术可能看起来没有必要。.NET的发布似乎促进了机器的抽象,对注重时间和成本的程序员来说好处更多。然而,如果你更感兴趣的是纯粹为了编写代码的乐趣而编写代码,而不是为了赚钱,那么你可能同样对编写快速代码感兴趣。根据你对当前机器的熟悉程度,你可以进行一些相当酷的优化,以榨取代码中每一个冗余的时钟周期。由于汇编语言的流行程度与机器码相当,阅读起来要困难得多,而且会让你“非常”依赖于机器,因此我将忽略在汇编中常见的优化。在任何高级语言中,你都可以进行一些简单的优化,以减小代码的大小和执行所需的周期;也许最被忽视的优化方法是所使用的算法。
以以下添加/删除/插入函数为例
Add (Index)
- 在当前索引的元素处添加一个项,并将数组中其后的元素向下移动一个位置。Del (Index)
- 从数组中删除索引处的元素,并将数组中其后的元素向上移动一个位置。Ins (Index)
- 在当前索引的元素处插入/覆盖一个项
Add(Index, newItem) { //Index MUST be within array bounds plus ONE element //ASSERT(Index<=UpperBound+1) //Allocate memory for one more element at end of array //Was allocation successful? Exit if not! //Push all items at or greater than Index down by one //Overwrite currently indexed element with newItem //Update counters } Del(Index) { //Pull all items greater than Index up by one to overwrite indexed element //Free memory used by last element //Update counters } Ins(Index, newItem) { //Index MUST be within array bounds plus ONE element //ASSERT(Index<=UpperBound+1) //Overwrite item at current index //Update counters }
如果以这种方式编写Insert
函数,将会导致代码膨胀;一个简单的解决冗余的方法是这样设计Insert()
:
Ins(Index, newItem) { Del(Index); Add(Index, newItem); }
注意:如果你希望Insert()
在索引超出范围时添加元素,而不仅仅是覆盖数组中当前索引处的元素,那么你需要添加更多的内存管理例程,这会使你的代码和管理复杂化。通过让Add()
来管理这一点,并让Insert()
调用Del()
和Add()
,你现在已经消除了许多不必要的代码,并将棘手的内存分配留给了2个函数而不是3个。如果你有一天决定使用虚拟内存而不是堆,那么管理起来会容易得多。你现在已经看到了一个糟糕的算法设计和冗余代码移除的例子,让我们继续讨论不太明显的代码调整技巧。
现在我们来谈谈速度增强,而不是代码大小
如果你曾经接触过任何旧的游戏/图形或汇编书籍,你经常会读到关于位移“更快的乘法/除法”。它是80x86指令集(集)中一个众所周知的技巧(我勉强用过去时,因为我不知道位移是否仍然更快),即移位是按2的幂进行除法或乘法的更快方法。
1
2631
8426 8421
=========
0000 0001 = 1
0000 0010 = 2 -> left-shift by one multiplied the value by two
0000 0100 = 4 -> left-shift by one multiplied the value by two again
当然,这只对整数除法/乘法有用。基本上,根据经验法则
int result = b * 2;
如果这样写会更快
int result = b << 1;
这些是任何支持位移的语言中一些常见、易于应用的优化。
以下是一些不需要深入了解计算机体系结构的其他优化。
移除循环不变量
for(int x=0;x<=10000;x++){ rect[x].left = nSpace + nWidth; }
像这样会更有效率
temp = nSpace + nWidth; for(int x=0;x<=10000;x++){ rect[x].left = temp; }
在第一个示例中,你进行了10001次不必要的加法计算,而第二个示例是在迭代数组之前计算变量。
公共子表达式消除
result = a*((a+b+c)+b*(a+b+c)+c);
像这样会更有效率
temp = a+b+c; result = a*(temp+b*temp+c);
在上面,你只计算了一次冗余表达式而不是两次(像第一个一样),并将其存储在一个中间变量中,因此你得到了更快(而且我认为)更小的编译代码。
这只是两个常见的代码优化技术,你可以轻松地应用到你自己的代码中。然而,还有不少技术你可以尝试,并且根据你的编译器,还有一些自动优化可供你使用。但为了简洁起见,我将忽略这些。
现在你对如何轻松优化代码有了一些了解,所以废话不多说,我将解释我的分析器是如何工作的(或者应该工作的),但首先是分析的基本知识。
什么是代码分析?
代码分析会记录你所分析的代码段或函数的时钟周期消耗。通过反复分析你的代码,你可以清楚地了解哪些函数或代码段导致了瓶颈或热点。一旦你定位了瓶颈,接下来的工作就是应用上面提到的(以及更多)技术来减少函数或代码段使用的时钟周期数量。
什么是时钟...?
时钟或时钟周期是你的CPU执行任务所消耗的指令。当你用HLL(高级语言)编写代码时,你的编译器会输出目标机器的本机指令列表。每条指令的执行都需要一定数量的时钟周期。假设ADD指令需要4个周期来执行,如果你的计算机是最新款的Pentium 4,额定频率为2.2 GHz(每秒2,200,000,000个周期),那么你的CPU每秒可以执行
每秒2200000000个周期 / ADD指令的4个时钟 = 550000000个ADD操作。这速度太快了,不是吗?
历史
在考虑了代码分析的实际工作原理后,我决定访问Intel的网站并下载他们的VTune程序(免费试用30天)。然而,在下载并玩了几分钟后,我决定它提供了太多用于简单分析我的MFC代码的功能、花哨的特性。我不是在设计“发现号”航天飞机的自动驾驶仪,为什么我需要所有这些信息?于是我坐下来,开始思考一个快速而粗糙的解决方案。自然而然地,记录从函数调用到函数返回的经过时间立即浮现在我的脑海中,但如何准确地做到这一点呢?我开始在CP上询问,许多人回答说GetTickCount
和QueryPerformanceCounter
。我知道GetTickCount
不是真正高性能的,所以我选择了QueryPerformanceCounter
。经过大约7个小时的开发和在网上搜索相关资源,我偶然发现了CodeGuru上的CProfile
……终于找到了一个例子。
我在CG上遇到的第一个例子使用了以下分析代码的方法。这是一个类,它使用GetTickCount
和两个成员函数Start和Finish(我想),并且可以选择将日志输出到磁盘或调试输出窗口。这个类的主要优点是,通过使用这两个函数,你可以更精确地定位你想要分析的内容;你对要分析的内容拥有完全的控制权。如果你确切地知道瓶颈在哪里,你就可以只分析那个代码……并且更好地观察你的改进。
如果你知道这个例子,你可能已经读了文章底部的评论,有人建议他开发了一个更好的分析类。这个类更面向对象(友好)?与其显式地提供要分析代码段的开始和结束,不如依赖构造函数开始记录,析构函数停止记录。这意味着你不太可能得到错误的分析时间,因为现在你可以确保正确地记录了开始和停止时间。如果你在另一个类中忘记了stop(),那么显然你将得到时钟消耗的不正确测量。这种方法的缺点是,你不再能直接控制要分析的代码段。分析在对象实例化时开始,在对象销毁或超出作用域时停止。这篇文章底部一个有趣的帖子中,另一个人说QueryPerformanceCounter
存在已知问题,并且不如最初设想的那样准确。
我做了一些更多的研究,发现了一些讨论QueryPerformanceCounter
函数的网站;它们都有负面评价,但没有什么具体的。凭着我的直觉,我决定多了解一下Intel自己的RTDSC指令,我很久以前读到过它。找到了解决方案!我将使用这个指令而不是2个Windows函数。然而,仍然存在一些缺点,因为我的CProfile
类将**仅**限于Intel平台!这没什么大不了的,因为我本来不打算发布代码,但既然我发布了,有些人可能会用它。抱歉!
我开发的CProfile
类使用RDTSC(读取时间戳计数器)指令……我相信这是最准确的方法(至少对于支持该架构的平台而言)。设计与第二个提到的类类似,时钟周期记录的开始和停止发生在构造和析构时。我只设计了针对DEBUG模式,所以你并不是用普通语法实例化对象,而是用提供的宏。这比另外2个类有一个很酷的优势。现在你可以在代码中的每个函数(在每个函数的最开始)插入一个PROFILER宏,并将其保留在那里……它就像一个ASSERT
或TRACE
宏……它只在调试模式下编译……在发布版本中不包含。
这三者(我认为)都创建日志文件……但没有工具来查看所有数据,日志文件有什么用呢?所以我写了一个快速而粗糙的工具来加载和查看分析数据。我很快就对这个项目失去了兴趣(现在有其他事情吸引我的注意力了),所以还有很多改进的空间。我的希望是,通过分享,你也会与我分享,并告诉我关于bug的信息;修复会很棒!我要求如果你添加或更改任何内容,请将更新后的代码发送给我,以便我能让这个项目继续下去。我一定会提及你的名字,我保证。
我非常感谢以下人员。不仅是这个项目,还有他们帮我解决的任何问题。帮助是这里的关键词,所以我不会在赚到数十亿的时候期待任何回报。
致谢
- Roger Wright - 校对了这篇文章,并向我展示了分号的正确用法,尽管我可能改动了一些
- Joaquin - 在编写我之前的一个缓冲区类时提供了巨大的帮助,向我展示了使用模板最酷、最高效的方法
- Christian Graus 和 Nish - 两人在提供我想要“今天”而不是“明天”的解决方案时,将我的开发速度提高了10倍。
- Kilowatt - 提醒了我
OnNcPaint
有时是绘制的好地方 - Rick York - 对初稿进行了一些更正,并提供了一些工具/信息,我计划在下次更新中包含这些。
当然还有CSortListCtrl
的作者,他做得很好!!!
已知bug
无……什么,你在开玩笑吗?
在实用程序程序中的某个地方,我认为我使用了一个long
而不是double
或__int64
。在处理时钟时,这可能意味着数据丢失。如果你找到了我认为我寻找的东西,请告知我。
除此之外,代码相当随意,所以如果你注意到缺乏const或清晰度,请告诉我,我会改变它,或者你可以自己改变并发送调整后的代码给我。我在开发后期意识到一个问题是,有些函数会显示很高的时钟数,这有点掩盖了其他被分析的函数。使用列表视图中的状态栏无济于事,并且不够准确,所以我计划设计一种图表来更准确地显示数据。正如我上面所说,有很多代码需要重做,所以请随意尝试。
我计划某天更新我的VC++当前版本到VC++ .NET,并且我想知道内联汇编是否仍然可用(有所有那些花哨的字节码东西)。如果你知道一种更面向未来的设计,请告诉我,我会开始“今天思考明天”。
Bug 修复
目前还没有!