好的,您正在将问题定义到看起来没有太大改进空间的地方。根据我的经验,这种情况相当罕见。我试图在 1993 年 11 月的 Dobbs 博士的一篇文章中解释这一点,从一个没有明显浪费的传统设计良好的非平凡程序开始,并对其进行一系列优化,直到它的挂钟时间从 48 秒减少到 1.1 秒,源代码大小减少了 4 倍。我的诊断工具 was this。变化的顺序是这样的:
发现的第一个问题是使用列表集群(现在称为“迭代器”和“容器类”)占一半以上的时间。这些被相当简单的代码取代,将时间缩短到 20 秒。
现在最大的耗时是建立列表。以百分比来说,以前没那么大,但现在是因为更大的问题被排除了。我想办法加快速度,时间降到了 17 秒。
现在很难找到明显的罪魁祸首,但有一些较小的罪魁祸首我可以做点什么,时间减少到 13 秒。
现在我好像碰壁了。样本准确地告诉我它在做什么,但我似乎找不到任何可以改进的地方。然后我反思了程序的基本设计,它的事务驱动结构,并询问它所做的所有列表搜索是否实际上是由问题的要求所要求的。
然后我进行了重新设计,其中程序代码实际上是从一组较小的源中生成的(通过预处理器宏),并且程序不会不断地找出程序员知道是相当可预测的东西。换句话说,不要“解释”要做的事情的顺序,“编译”它。
- 重新设计完成,源代码缩小了 4 倍,时间缩短到 10 秒。
现在,因为它变得如此之快,所以很难采样,所以我给它 10 倍的工作量,但以下时间是基于原始工作量。
更多诊断表明它正在花费时间进行队列管理。内联这些将时间减少到 7 秒。
现在,我一直在做的诊断打印需要花费大量时间。冲洗 - 4 秒。
现在最耗时的是调用 malloc 和 free。回收对象 - 2.6 秒。
继续采样,我仍然发现并非绝对必要的操作 - 1.1 秒。
总加速因子:43.6
现在没有两个程序是相同的,但在非玩具软件中,我总是看到这样的进展。首先你得到容易的东西,然后是更难的东西,直到你达到收益递减的地步。然后你获得的洞察力很可能会导致重新设计,开始新一轮的加速,直到你再次达到收益递减。现在,可能有必要怀疑++i 或i++ 或for(;;) 或while(1) 是否更快:我在 StackOverflow 上经常看到的这类问题。
附:可能想知道为什么我没有使用分析器。答案是这些“问题”中的几乎每一个都是一个函数调用站点,堆栈样本可以精确定位。即使在今天,分析器也几乎没有意识到语句和调用指令比整个函数更重要且更容易修复。
我实际上构建了一个分析器来执行此操作,但要真正了解代码正在执行的操作,没有什么可以替代您的手指正确的操作。样本数量少不是问题,因为发现的问题都不是小到容易漏掉的问题。
添加:jerryjvl 要求提供一些示例。这是第一个问题。它由少量单独的代码行组成,总共占用了一半以上的时间:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
这些是使用列表集群 ILST(类似于列表类)。它们以通常的方式实现,“信息隐藏”意味着该类的用户不必关心它们是如何实现的。在编写这些行时(大约 800 行代码),并没有想到这些可能是“瓶颈”(我讨厌这个词)。它们只是推荐的做事方式。 事后看来很容易说这些应该避免,但根据我的经验所有性能问题都是这样的。一般来说,最好尽量避免产生性能问题。即使“应该避免”(事后看来),找到并修复已创建的那些甚至更好。我希望这能给你一点味道。
这是第二个问题,分两行:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
这些是通过将项目附加到其末尾来构建列表。 (解决方法是收集数组中的项目,并一次构建所有列表。)有趣的是,这些语句仅花费(即在调用堆栈上)原始时间的 3/48,因此它们不在事实上一个大问题一开始。然而,在消除第一个问题后,它们花费了 3/20 的时间,因此现在是一条“更大的鱼”。总的来说,就是这样。
我可能会补充一点,这个项目是从我帮助过的一个真实项目中提炼出来的。在那个项目中,性能问题更加严重(加速也是如此),例如在内部循环中调用数据库访问例程以查看任务是否完成。
已添加参考:
原始和重新设计的源代码可以在www.ddj.com 中找到,1993 年的文件 9311.zip、文件 slug.asc 和 slug.zip。
编辑 2011/11/26:
现在有一个 SourceForge project 包含 Visual C++ 中的源代码,以及它是如何调整的详细描述。它只经历了上述场景的前半部分,并没有遵循完全相同的顺序,但仍然获得了 2-3 个数量级的加速。