在这个网站上已经有很多性能问题了,但是在我看来,几乎所有的问题都是非常具体的,而且相当狭窄。几乎所有人都重复了避免过早优化的建议。

我们假设:

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

我在这里寻找的是策略和技巧,在一个关键算法中,当没有其他事情可做,但无论如何都要挤出最后百分之几。

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

我将添加一个带有我自己最初建议的回复,并期待Stack Overflow社区能想到的任何其他东西。


当前回答

您可能应该考虑“谷歌视角”,即确定您的应用程序如何在很大程度上实现并行和并发,这也不可避免地意味着在某种程度上考虑将您的应用程序分布在不同的机器和网络上,这样它就可以理想地与您投入的硬件几乎线性扩展。

另一方面,谷歌人员也以投入大量人力和资源来解决他们正在使用的项目、工具和基础设施中的一些问题而闻名,例如,通过拥有一个专门的工程师团队来破解gcc内部,以便为Google典型的用例场景做好准备,从而对gcc进行整个程序优化。

类似地,分析应用程序不再仅仅意味着分析程序代码,还包括它周围的所有系统和基础设施(想想网络、交换机、服务器、RAID阵列),以便从系统的角度识别冗余和优化潜力。

其他回答

OK, you're defining the problem to where it would seem there is not much room for improvement. That is fairly rare, in my experience. I tried to explain this in a Dr. Dobbs article in November 1993, by starting from a conventionally well-designed non-trivial program with no obvious waste and taking it through a series of optimizations until its wall-clock time was reduced from 48 seconds to 1.1 seconds, and the source code size was reduced by a factor of 4. My diagnostic tool was this. The sequence of changes was this:

The first problem found was use of list clusters (now called "iterators" and "container classes") accounting for over half the time. Those were replaced with fairly simple code, bringing the time down to 20 seconds. Now the largest time-taker is more list-building. As a percentage, it was not so big before, but now it is because the bigger problem was removed. I find a way to speed it up, and the time drops to 17 seconds. Now it is harder to find obvious culprits, but there are a few smaller ones that I can do something about, and the time drops to 13 sec.

现在我似乎遇到了瓶颈。样本告诉我它到底在做什么,但我似乎找不到任何可以改进的地方。然后,我考虑了程序的基本设计及其事务驱动结构,并询问它所做的所有列表搜索实际上是否都是由问题的需求强制执行的。

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

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

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

进一步的诊断表明,它是在队列管理上花费时间的。内联这些将时间缩短到7秒。 现在一个很大的时间消耗是我一直在做的诊断打印。冲水- 4秒 现在最浪费时间的是调用malloc和free。回收对象- 2.6秒。 继续进行抽样,我仍然发现了严格意义上没有必要的操作——1.1秒。

总加速系数:43.6

Now no two programs are alike, but in non-toy software I've always seen a progression like this. First you get the easy stuff, and then the more difficult, until you get to a point of diminishing returns. Then the insight you gain may well lead to a redesign, starting a new round of speedups, until you again hit diminishing returns. Now this is the point at which it might make sense to wonder whether ++i or i++ or for(;;) or while(1) are faster: the kinds of questions I see so often on Stack Overflow.

附注:可能有人想知道我为什么不用侧写器。答案是,几乎所有这些“问题”都是函数调用站点,堆栈样本可以精确定位。即使在今天,分析人员也只是勉强接受这样一个观点:语句和调用指令比整个函数更重要,更容易定位,也更容易修复。

我实际上构建了一个剖析器来做这件事,但是要真正了解代码正在做什么,没有什么可以替代您的手指。样本数量少并不是问题,因为被发现的问题没有一个小到容易被忽略的程度。

添加: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)

These were using the list cluster ILST (similar to a list class). They are implemented in the usual way, with "information hiding" meaning that the users of the class were not supposed to have to care how they were implemented. When these lines were written (out of roughly 800 lines of code) thought was not given to the idea that these could be a "bottleneck" (I hate that word). They are simply the recommended way to do things. It is easy to say in hindsight that these should have been avoided, but in my experience all performance problems are like that. In general, it is good to try to avoid creating performance problems. It is even better to find and fix the ones that are created, even though they "should have been avoided" (in hindsight). I hope that gives a bit of the flavor.

下面是第二个问题,分两行:

 /* 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, files slug。Asc和slug.zip。

编辑2011/11/26: 现在有一个SourceForge项目包含了Visual c++中的源代码,以及它是如何调优的详细描述。它只经历了上述场景的前半部分,并不完全遵循相同的顺序,但仍然获得了2-3个数量级的加速。

当你不能再提高表现时,看看你是否可以提高感知的表现。

您可能无法使您的fooCalc算法更快,但通常有一些方法可以使您的应用程序对用户的响应更快。

举几个例子:

预测用户将要做什么 请求并开始着手这项工作 在那之前 将结果显示为 它们是进来的,而不是同时出现的 在最后 精确的进度计

这些不会让你的程序更快,但可能会让你的用户对你的速度更满意。

减少可变大小(在嵌入式系统中)

如果您的变量大小大于特定体系结构上的单词大小,则会对代码大小和速度产生重大影响。例如,如果你有一个16位系统,经常使用一个长int变量,然后意识到它永远不能超出范围(−32.768…32.767)考虑将其减少到短int。

从我的个人经验来看,如果一个程序已经准备好或几乎准备好了,但是我们意识到它占用了目标硬件程序内存的110%或120%,那么对变量进行快速归一化通常可以解决这个问题。

到这个时候,优化算法或部分代码本身可能会变得令人沮丧的徒劳:

重新组织整个结构,程序就不再像预期的那样工作,或者至少引入了许多错误。 做一些聪明的技巧:通常你花了很多时间优化一些东西,并发现代码大小没有或很小的减少,因为编译器无论如何都会优化它。

Many people make the mistake of having variables which exactly store the numerical value of a unit they use the variable for: for example, their variable time stores the exact number of milliseconds, even if only time steps of say 50 ms are relevant. Maybe if your variable represented 50 ms for each increment of one, you would be able to fit into a variable smaller or equal to the word size. On an 8 bit system, for example, even a simple addition of two 32-bit variables generates a fair amount of code, especially if you are low on registers, while 8 bit additions are both small and fast.

更多的建议:

Avoid I/O: Any I/O (disk, network, ports, etc.) is always going to be far slower than any code that is performing calculations, so get rid of any I/O that you do not strictly need. Move I/O up-front: Load up all the data you are going to need for a calculation up-front, so that you do not have repeated I/O waits within the core of a critical algorithm (and maybe as a result repeated disk seeks, when loading all the data in one hit may avoid seeking). Delay I/O: Do not write out your results until the calculation is over, store them in a data structure and then dump that out in one go at the end when the hard work is done. Threaded I/O: For those daring enough, combine 'I/O up-front' or 'Delay I/O' with the actual calculation by moving the loading into a parallel thread, so that while you are loading more data you can work on a calculation on the data you already have, or while you calculate the next batch of data you can simultaneously write out the results from the last batch.

目前最重要的限制因素是有限的内存带宽。多核只会让情况变得更糟,因为带宽是在核之间共享的。此外,用于实现缓存的有限芯片区域也分配给了内核和线程,这进一步恶化了这个问题。最后,保持不同缓存一致性所需的芯片间信号也会随着核数的增加而增加。这也增加了一个惩罚。

这些是您需要管理的影响。有时是通过对代码的微观管理,但有时是通过仔细考虑和重构。

很多注释已经提到了缓存友好的代码。至少有两种不同的风格:

避免内存读取延迟。 降低内存总线压力(带宽)。

第一个问题与如何使数据访问模式更规则有关,从而使硬件预取器更有效地工作。避免动态内存分配,这会将数据对象分散在内存中。使用线性容器代替链表、散列和树。

第二个问题与提高数据重用有关。修改算法以处理适合可用缓存的数据子集,并在数据仍在缓存中时尽可能多地重用这些数据。

更紧密地封装数据并确保在热循环中使用缓存线路中的所有数据,将有助于避免这些其他影响,并允许在缓存中安装更多有用的数据。