代码优化

11
你被赋予一堆你最喜欢的编程语言代码,这些代码组合在一起形成了一个相当复杂的应用程序。它运行得相当慢,你的老板要求你进行优化。你会采取哪些步骤来最有效地优化代码? 在优化代码时,你发现哪些策略是不成功的重写:你何时决定停止优化并说“如果不完全重写,它就只能运行这么快了”?在什么情况下,您会提倡进行简单的完全重写?您将如何设计它?

我应该将这个问题拆分成多个吗? - Claudiu
我认为这是一个很好的问题。它实际上是一堆概念的集合,最好作为一个整体来处理。 - Mike Dunlavey
12个回答

42

在尝试任何优化之前,请了解当前的情况。

十有八九,时间并不会消耗在你猜测的地方。

通常不成功的策略是微观优化,实际需要的是适当的算法。

必须引用唐纳德·库斯(Donald Knuth)的话:

"我们应该忘记小优化,大约有97%的时间: 过早的优化是万恶之源"


2
同意,没有基准线进行优化是无用的。如果你不能测量它,你就不能改进它。 - paxdiablo

22

步骤:

  1. 测量
  2. 分析
  3. 决策
  4. 实施
  5. 重复

首先,获得一个性能分析器来测量代码。不要陷入认为你知道瓶颈在哪里的陷阱中。即使之后你的假设被证明是正确的,也不要认为下一次有类似任务时可以跳过测量步骤。

然后,分析您的发现。查看代码,识别出您可以对其进行改进以产生效果的瓶颈。尝试估计这将带来多少改进。

根据您的分析决定是否要走这条路。收益是否值得?是否需要重写代码?也许您会发现,虽然运行速度很慢,但已经达到了极限,或者您接近性能曲线的顶部,在那里需要付出巨大努力才能获得微小的改进。

实施您的更改,必要时进行重写,或者如果您已经选择了重构路径,则重构代码。逐步进行,以便于测量您的更改是否达到预期,或者您是否需要回退一步并尝试另一种方法。

然后回到开始,重新测量、分析、决策、实施等。

另外,在重构代码的时候,首先应该更改的是大型算法级别的方法。例如,用性能更好的排序算法替换原来的算法等。不要从行级别的优化开始,比如如何让递增值的行更快。这些通常是最后级别的优化,除非你正在极端性能条件下运行,否则通常不值得。


5
你忘记了0:定义如果没有“慢”、“快”、“足够快”、“性能”、“速度”等术语的明确定义,你就永远不会知道何时完成。事实上,你甚至无法确定是否有任何进展。此外,由于不知道要测量什么,你也无法完成#1。 - Jörg W Mittag

8
不要试图做任何事情而不使用某种类型的分析工具,我无法强调这一点!不幸的是,我所知道的所有分析工具要么不好用,要么很昂贵(商业费用太高了!),所以我会让其他人在这方面提出建议 :)。
当你的数据告诉你需要改写时,你就知道你需要改写了。这听起来有些递归,但实际上只是意味着你目前的架构或软件堆栈的成本已经足以把你推向性能崖边,因此你所做的局部更改都无法解决整体问题。然而,在你打开“文件”->“新建...”命令之前,请制定一个计划,建立一个原型,并测试原型是否比当前系统在完成同样任务时表现更好:惊人的是,很少有明显的差异!

非常好的关于重写的说法,特别是关于堆栈成本高于程序的部分。 - fluffels
我同意,Simon。就个人而言,我将“性能分析”分为两个目的。第一个是量化任何变化。第二个是“找到”问题。人们喜欢工具,但在我的回答中(下面),我已经解释了我如何做到这一点。 - Mike Dunlavey

4

首先不要忘记这些:

  • 过早优化是万恶之源
  • 性能来自设计

其次;

不要假设,试一试

我认为这是优化的基本规则,测试它,除非你测试并证明它有效,否则你不会知道。

在您的情况下,我会首先重构代码,了解它。

如果您有单元测试,那么很幸运,只需逐个函数进行检查,并具体查看最常调用的代码(使用分析工具观察调用和瓶颈所在)。 如果您没有测试,请编写一些基本测试以确认某些条件下的整体输出,以确保您不会破坏任何内容并且可以自由尝试


1

除了性能分析,正如每个人都提到的那样,我总是首先考虑记忆化和延迟加载这两个解决方案(在性能分析之后),它们都很容易实现并且通常会产生很大的差异。


1

所有的答案都很好。

我想要完善一下建议中的“测量”部分。我进行测量是为了量化我可能做出的任何改进。然而,为了找到需要修复的问题(这是不同的目的),我会手动获取多个调用堆栈样本。

假设,为简单起见,程序运行需要20吉赫周期,但实际上需要10个周期。如果我随机暂停它10次,那么在其中5次左右,它将处于其中一个不必要的周期中。我可以通过查看每个调用堆栈层来确定该周期是否必要。如果在堆栈的任何级别上有任何调用指令可以被消除,则该周期是不必要的。如果这样的指令出现在多个堆栈上,则消除它加速程序,加速的百分比大约等于它所在的堆栈样本的百分比。

任何出现在多个堆栈上的指令都是可疑的——堆栈越多,可疑性越高。现在,call _main可能是我无法删除的一个指令,但是
foo.cpp:96 call std::vector::iterator:++
如果它在多个堆栈上出现,那么它绝对是一个需要关注的焦点。

出于风格原因,有些人可能不想替换它,但是他们会大致知道为这个选择付出了多少性能代价。

因此,优化的过程就是识别嫌疑对象并找到一种替换或消除它们的方法。

困难的部分(我已经做过很多次)是理解什么是必要的,什么是不必要的。为此,您需要了解在该程序中如何以及为什么执行某些操作,这样,如果某个活动是一个循环占用资源的问题,您就可以知道如何安全地替换它。

有些循环占用资源的问题可能很容易解决,但您很快就会遇到一些您不知道如何安全替换的问题。为此,您需要更熟悉代码。

如果您可以向已经在该程序上工作过的人请教,那将会很有帮助。

否则(假设代码约为10^6行,就像我曾经处理过的那样),您可以相对容易地加速一些操作,但要超越这个范围,可能需要几个月的时间才能达到您舒适地进行重大更改的水平。


0

好的策略

除了提到的基本优化法则(测量,如果不必要就不要进行优化),尽管问题明确要求效率,但我总是在检查期间对这样的代码进行重构

通常,性能差的代码也很难理解。因此,在重构时,我会确保代码本身更易于理解和更好地记录文档。这是确保我知道我正在优化什么的基础(因为在大多数情况下,该代码片段的要求也不会完全可用)。

何时停止

对于运行非常缓慢的应用程序,您通常会在分析器中显示单个方法(或一组相关方法)的运行时间出现峰值,从而显示编程错误或设计缺陷。因此,如果分析方法的运行时间分布大致相等(或者显示的瓶颈方法的大部分是平台代码,例如Sun Java方法),我通常会停止。如果客户需要进一步优化,则必须重新设计应用程序的较大部分,而不是优化现有代码。


0

一定要有足够的单元测试,以确保您进行的任何优化都不会破坏任何东西。

确保限定您的执行环境。有时,执行选项的简单更改可以大有作为。

然后,才开始分析代码。

重写决策(针对已经工作的代码)只有在当前架构可能无法支持足够的未来发展时才应该考虑。
如果简单的修复可以加速一个不太可能发展的代码,那么就不需要完全重写。

通常与最终用户(客户端)合作确定停止标准,但我建议使用正式文件来确定此优化过程的目标。


0

首先确定您的优化目标 - 为给定硬件平台上特定操作的时间设置目标。准确地测量性能(确保您的结果可重复)并在类似生产环境的环境中进行测试(除非这是您在生产中使用的VM等)。

然后,如果您认为速度已经足够快,那么可以停止优化。

如果仍然不够好,则需要进行一些额外的工作 - 这就是分析的作用。您可能无法很好地使用分析器(例如,如果它会对行为产生影响),在这种情况下,应改用仪表化。


0
假设代码需要优化,那么我会使用以下方法: 1.) 优化缓冲区处理方式 - 缓存优化。缓存延迟是一个非常耗费CPU周期的领域,大约在5-10%的开销范围内。因此,要以高效的方式利用数据缓冲区。
2.) 关键循环和MCPS密集型函数可以使用汇编语言编写,或者使用编译器为该硬件目标提供的低级别内部函数。
3.) 从外部内存读取/写入是很耗费周期的。尽可能减少对外部内存的访问。或者如果必须访问,则以高效的方式进行(预加载数据,并行DMA访问等)。
通常,如果通过遵循优化技术获得了约20%的优化(最佳情况),我认为这已经足够好,并且没有进行任何主要的代码重构、算法重新设计。之后就变得棘手、复杂和繁琐了。
-AD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接