小编典典

不得已的性能优化策略

all

这个网站上已经有很多性能问题,但我发现几乎所有问题都非常具体且相当狭窄。几乎所有的人都会重复这些建议以避免过早的优化。

让我们假设:

  • 代码已经正常工作
  • 选择的算法对于问题的情况已经是最优的
  • 代码已被测量,并且有问题的例程已被隔离
  • 还将衡量所有优化尝试,以确保它们不会使事情变得更糟

我在这里寻找的是策略和技巧,可以在没有其他事情可做的情况下,在关键算法中挤出到最后几个百分比,而不管它需要什么。

理想情况下,尝试使答案与语言无关,并在适用的情况下指出建议策略的任何缺点。


阅读 148

收藏
2022-03-06

共1个答案

小编典典

好的,您正在将问题定义到看起来没有太大改进空间的地方。根据我的经验,这种情况相当罕见。我试图在 1993 年 11 月的 Dobbs
博士的一篇文章中解释这一点,从一个没有明显浪费的传统设计良好的非平凡程序开始,并对其进行一系列优化,直到它的挂钟时间从 48 秒减少到 1.1
秒,源代码大小减少了 4 倍。我的诊断工具是这个。变化的顺序是这样的:

  • 发现的第一个问题是使用列表集群(现在称为“迭代器”和“容器类”)占一半以上的时间。这些被相当简单的代码所取代,将时间缩短到 20 秒。

  • 现在最大的耗时是更多的列表建设。以百分比来说,以前没那么大,但现在是因为更大的问题被排除了。我找到了一种加快速度的方法,时间降到了 17 秒。

  • 现在很难找到明显的罪魁祸首,但我可以做一些较小的罪魁祸首,时间下降到 13 秒。

现在我好像碰壁了。样本准确地告诉我它在做什么,但我似乎找不到任何可以改进的地方。然后我反思了程序的基本设计,它的事务驱动结构,并询问它所做的所有列表搜索是否实际上是由问题的要求所要求的。

然后我遇到了重新设计,其中程序代码实际上是从一组较小的源中生成的(通过预处理器宏),并且程序不会不断地找出程序员知道是相当可预测的东西。换句话说,不要“解释”要做的事情的顺序,“编译”它。

  • 重新设计完成,源代码缩小了 4 倍,时间减少到 10 秒。

现在,因为它变得如此之快,很难采样,所以我给它 10 倍的工作量,但以下时间是基于原始工作量。

  • 更多诊断表明它正在花费时间进行队列管理。内联这些将时间减少到 7 秒。

  • 现在,我一直在做的诊断打印是一个很大的耗时。冲洗 - 4 秒。

  • 现在最大的耗时是调用 mallocfree 。回收对象 - 2.6 秒。

  • 继续采样,我仍然发现并非绝对必要的操作——1.1 秒。

总加速因子:43.6

现在没有两个程序是一样的,但在非玩具软件中,我总是看到这样的进展。首先你得到简单的东西,然后是更难的东西,直到你达到收益递减的地步。然后你获得的洞察力很可能会导致重新设计,开始新一轮的加速,直到你再次达到收益递减。现在这可能是想知道++ior
i++or or for(;;)orwhile(1)是否更快的点:我经常在 Stack_Overflow 上看到的这类问题。

PS可能想知道为什么我没有使用分析器。答案是这些“问题”中的几乎每一个都是一个函数调用站点,堆栈样本可以精确定位。即使在今天,分析器也几乎没有意识到语句和调用指令比整个函数更重要,更容易修复。

我实际上构建了一个分析器来执行此操作,但是为了真正了解代码正在执行的操作,没有什么可以替代您的手指正确的操作。样本数量少不是问题,因为发现的问题都不是小到容易漏掉的问题。

添加: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
项目
,其中包含 Visual C++
中的源代码以及如何调整它的详细描述。它只经历了上述场景的前半部分,并没有遵循完全相同的顺序,但仍然获得了 2-3 个数量级的加速。

2022-03-06