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

代码分析器和优化

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.74/5 (16投票s)

2002年3月28日

10分钟阅读

viewsIcon

226983

downloadIcon

1615

C++ 代码分析器和小型分析实用程序

Sample Image - Profiler.jpg

引言

时下台式机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上询问,许多人回答说GetTickCountQueryPerformanceCounter。我知道GetTickCount不是真正高性能的,所以我选择了QueryPerformanceCounter。经过大约7个小时的开发和在网上搜索相关资源,我偶然发现了CodeGuru上的CProfile……终于找到了一个例子。

我在CG上遇到的第一个例子使用了以下分析代码的方法。这是一个类,它使用GetTickCount和两个成员函数Start和Finish(我想),并且可以选择将日志输出到磁盘或调试输出窗口。这个类的主要优点是,通过使用这两个函数,你可以更精确地定位你想要分析的内容;你对要分析的内容拥有完全的控制权。如果你确切地知道瓶颈在哪里,你就可以只分析那个代码……并且更好地观察你的改进。

如果你知道这个例子,你可能已经读了文章底部的评论,有人建议他开发了一个更好的分析类。这个类更面向对象(友好)?与其显式地提供要分析代码段的开始和结束,不如依赖构造函数开始记录,析构函数停止记录。这意味着你不太可能得到错误的分析时间,因为现在你可以确保正确地记录了开始和停止时间。如果你在另一个类中忘记了stop(),那么显然你将得到时钟消耗的不正确测量。这种方法的缺点是,你不再能直接控制要分析的代码段。分析在对象实例化时开始,在对象销毁或超出作用域时停止。这篇文章底部一个有趣的帖子中,另一个人说QueryPerformanceCounter存在已知问题,并且不如最初设想的那样准确。

我做了一些更多的研究,发现了一些讨论QueryPerformanceCounter函数的网站;它们都有负面评价,但没有什么具体的。凭着我的直觉,我决定多了解一下Intel自己的RTDSC指令,我很久以前读到过它。找到了解决方案!我将使用这个指令而不是2个Windows函数。然而,仍然存在一些缺点,因为我的CProfile类将**仅**限于Intel平台!这没什么大不了的,因为我本来不打算发布代码,但既然我发布了,有些人可能会用它。抱歉!

我开发的CProfile类使用RDTSC(读取时间戳计数器)指令……我相信这是最准确的方法(至少对于支持该架构的平台而言)。设计与第二个提到的类类似,时钟周期记录的开始和停止发生在构造和析构时。我只设计了针对DEBUG模式,所以你并不是用普通语法实例化对象,而是用提供的宏。这比另外2个类有一个很酷的优势。现在你可以在代码中的每个函数(在每个函数的最开始)插入一个PROFILER宏,并将其保留在那里……它就像一个ASSERTTRACE宏……它只在调试模式下编译……在发布版本中不包含。

这三者(我认为)都创建日志文件……但没有工具来查看所有数据,日志文件有什么用呢?所以我写了一个快速而粗糙的工具来加载和查看分析数据。我很快就对这个项目失去了兴趣(现在有其他事情吸引我的注意力了),所以还有很多改进的空间。我的希望是,通过分享,你也会与我分享,并告诉我关于bug的信息;修复会很棒!我要求如果你添加或更改任何内容,请将更新后的代码发送给我,以便我能让这个项目继续下去。我一定会提及你的名字,我保证。

我非常感谢以下人员。不仅是这个项目,还有他们帮我解决的任何问题。帮助是这里的关键词,所以我不会在赚到数十亿的时候期待任何回报。

致谢

  • Roger Wright - 校对了这篇文章,并向我展示了分号的正确用法,尽管我可能改动了一些
  • Joaquin - 在编写我之前的一个缓冲区类时提供了巨大的帮助,向我展示了使用模板最酷、最高效的方法
  • Christian Graus 和 Nish - 两人在提供我想要“今天”而不是“明天”的解决方案时,将我的开发速度提高了10倍。
  • Kilowatt - 提醒了我OnNcPaint有时是绘制的好地方
  • Rick York - 对初稿进行了一些更正,并提供了一些工具/信息,我计划在下次更新中包含这些。

当然还有CSortListCtrl的作者,他做得很好!!!

已知bug

无……什么,你在开玩笑吗?

在实用程序程序中的某个地方,我认为我使用了一个long而不是double__int64。在处理时钟时,这可能意味着数据丢失。如果你找到了我认为我寻找的东西,请告知我。

除此之外,代码相当随意,所以如果你注意到缺乏const或清晰度,请告诉我,我会改变它,或者你可以自己改变并发送调整后的代码给我。我在开发后期意识到一个问题是,有些函数会显示很高的时钟数,这有点掩盖了其他被分析的函数。使用列表视图中的状态栏无济于事,并且不够准确,所以我计划设计一种图表来更准确地显示数据。正如我上面所说,有很多代码需要重做,所以请随意尝试。

我计划某天更新我的VC++当前版本到VC++ .NET,并且我想知道内联汇编是否仍然可用(有所有那些花哨的字节码东西)。如果你知道一种更面向未来的设计,请告诉我,我会开始“今天思考明天”。

Bug 修复

目前还没有!

© . All rights reserved.