最后手段的性能优化策略

Performance optimization strategies of last resort

在这个网站上已经有很多性能问题了,但我突然想到,几乎所有的问题都是特定的,而且相当狭窄。几乎所有人都重复这些建议以避免过早的优化。

让我们假设:

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

我在这里寻找的是一些策略和技巧,在关键的算法中,当没有其他的事情可以做,但是需要做什么的时候,可以挤出到最后几个百分点。

理想情况下,尽量使答案语言不可知论,并指出建议的策略的任何向下方(如适用)。

我将添加一个带有我自己初始建议的回复,并期待堆栈溢出社区能够想到的其他内容。


好吧,你定义的问题似乎没有太大的改进空间。根据我的经验,这是相当罕见的。我试图在93年11月的Dobbs博士的文章中解释这一点,从一个设计良好、没有明显浪费的传统程序开始,并对它进行一系列优化,直到它的挂钟时间从48秒减少到1.1秒,源代码大小减少了4倍。我的诊断工具就是这个。变化的顺序是:好的。

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

  • 现在最大的时间接受者是更多的列表构建。作为一个百分比,它以前并没有那么大,但现在是因为更大的问题被消除了。我想办法加快速度,时间降到17秒。好的。

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

现在我好像撞到墙上了。样品告诉我它在做什么,但我似乎找不到任何可以改进的东西。然后,我回顾了程序的基本设计,它的事务驱动结构,并询问它正在执行的所有列表搜索是否实际上是由问题的需求所强制执行的。好的。

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

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

现在,因为它变得很快,很难取样,所以我给它10倍的工作要做,但是下面的时间是基于最初的工作量。好的。

  • 更多的诊断表明它在队列管理中花费了时间。在排队时,这会将时间缩短到7秒。好的。

  • 现在一个大的接受者是我做过的诊断打印。冲洗-4秒。好的。

  • 现在最大的时间接受者是打给malloc和free的电话。回收对象-2.6秒。好的。

  • 继续采样,我仍然发现不需要严格的操作-1.1秒。好的。

总加速系数:43.6好的。

现在没有两个程序是相同的,但在非玩具软件中,我总是看到这样的进展。首先你得到容易的东西,然后更难,直到你得到一个递减的回报点。然后,你获得的洞察力很可能会导致重新设计,开始新一轮的加速,直到你再次达到递减回报。现在,这一点可能有理由怀疑++ii++for(;;)while(1)是否更快:我经常看到的问题种类如此之多。好的。

另外,我可能想知道为什么我没有使用分析器。答案是,这些"问题"中几乎每一个都是一个函数调用站点,堆栈样本可以精确定位。即使在今天,分析人员也很少想到语句和调用指令比整个函数更重要、更容易修复。实际上,我构建了一个分析器来实现这一点,但是对于代码所做的真正的低调和肮脏的亲密关系,没有任何替代方法可以让您的手指直接插入其中。样本数量不小不是问题,因为所发现的问题都不小,很容易遗漏。好的。

补充:Jerryjvl要求一些例子。这是第一个问题。它由少量单独的代码行组成,总共占用了一半的时间:好的。

1
2
3
4
5
6
7
8
 /* 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行代码)时,人们并没有想到这些行可能是一个"瓶颈"(我讨厌这个词)。它们只是推荐的方法。事后看来,很容易说应该避免这些问题,但根据我的经验,所有性能问题都是这样的。一般来说,最好避免造成性能问题。更好的办法是找到并修复那些被创造出来的东西,即使它们"应该被避免"(事后看来)。希望能给你一点味道。好的。

下面是第二个问题,分为两行:好的。

1
2
3
4
5
 /* 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:现在有一个包含VisualC++中的源代码的SooCuFrand项目,以及如何调整它的详细描述。它只经历了上面描述的场景的前半部分,它不遵循完全相同的顺序,但仍然得到2-3个数量级的加速。好的。好啊。


建议:

  • 预计算而不是重新计算:任何包含输入范围相对有限的计算的循环或重复调用,请考虑对有效输入范围内的所有值进行包含计算结果的查找(数组或字典)。然后在算法内部使用简单的查找。缺点:如果实际使用的预计算值很少,这可能会使情况更糟,而且查找可能需要大量的内存。
  • 不要使用库方法:大多数库都需要编写来在广泛的场景下正确地运行,并对参数执行空检查等。通过重新实现一个方法,您可能能够去掉许多不适用于您使用它的具体环境的逻辑。缺点:编写额外的代码意味着更多的bug表面积。
  • 一定要使用图书馆的方法:为了自相矛盾,语言图书馆是由比你或我聪明得多的人编写的;很可能他们做得越来越好。不要自己实现它,除非你能使它更快(即:总是测量!)
  • 欺骗:在某些情况下,尽管可能存在针对您的问题的精确计算,但您可能不需要"精确",有时近似值可能"足够好",并且在交易中速度更快。问问你自己,如果答案是1%,那真的有关系吗?5%?甚至10%?下半身:嗯……答案不准确。


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

您可能无法使FOOCALC算法更快,但通常有一些方法可以使您的应用程序看起来对用户更具响应性。

举几个例子:

  • 预测用户要做什么请求并开始工作在那之前
  • 结果显示为他们进来了,而不是同时进来最后
  • 精密进度表

这些不会使您的程序更快,但它可能会使您的用户更高兴与您的速度。


我一生的大部分时间都在这个地方度过。广泛的笔触是运行你的分析器并让它记录:

  • 高速缓存未命中。数据缓存是大多数程序中暂停的1个来源。通过重新组织有问题的数据结构以获得更好的位置来提高缓存命中率;将结构和数字类型压缩以消除浪费的字节(从而消除浪费的缓存提取);尽可能预取数据以减少暂停。
  • 加载命中的商店。编译器对指针别名的假设,以及通过内存在断开连接的寄存器集之间移动数据的情况,可能会导致某些病理行为,从而导致整个CPU管道在加载操作上清除。查找浮动、向量和int相互转换的位置并消除它们。大量使用__restrict向编译器承诺别名。
  • 微码操作。大多数处理器都有一些不能流水线操作的操作,而是运行一个存储在ROM中的小子程序。PowerPC上的例子是整数乘、除和移位。问题是,在执行此操作时,整个管道将停止运行。尝试消除这些操作的使用,或者至少将它们分解为组成管道操作,这样无论程序的其余部分在做什么,您都可以从超标量调度中获益。
  • 分支预测失误。这些太空了。找到CPU在分支后花费大量时间重新填充管道的情况,并使用分支提示(如果可用)使其更频繁地正确预测。或者更好的方法是,尽可能用条件移动来替换分支,尤其是在浮点操作之后,因为它们的管道通常更深,并且在fcmp之后读取条件标志会导致暂停。
  • 顺序浮点运算。做这些模拟人生。

还有一件事我喜欢做:

  • 将编译器设置为输出程序集列表,并查看它为代码中的热点函数发出的内容。所有那些"优秀的编译器应该能够自动为您做的聪明的优化"?很有可能实际的编译器不这么做。我见过gcc发出真正的wtf代码。


扔更多的硬件!


更多建议:

  • 避免I/O:任何I/O(磁盘、网络、端口等)都是总是比任何代码都慢得多执行计算,因此去掉您所做的任何I/O不必严格要求。

  • 将I/O移到前面:加载要传输的所有数据需要预先计算,这样你就不会在关键算法(可能是重复的磁盘查找,当在一次命中中加载所有数据可能会避免查找)。

  • 延迟I/O:在计算结束,将它们存储在数据结构中,那就把它一次扔出去,当辛苦的工作结束的时候完成了。

  • 线程I/O:对于足够大胆的人,将I/O组合起来"预先"或"延迟I/O",实际计算方式为将负载移动到平行螺纹中,以便您正在加载更多的数据,可以对其进行计算您已经拥有的数据,或者在计算下一个数据时一批数据可以同时写出结果从最后一批。


由于许多性能问题涉及数据库问题,所以在优化查询和存储过程时,我将提供一些具体的内容供您参考。

在大多数数据库中避免使用光标。也避免循环。大多数情况下,数据访问应该基于设置,而不是按记录处理。这包括当您希望一次插入1000000条记录时,不重用单个记录存储过程。

永远不要使用select*,只返回您实际需要的字段。如果存在任何联接,这尤其正确,因为联接字段将重复,从而在服务器和网络上造成不必要的负载。

避免使用相关的子查询。使用联接(在可能的情况下包括到派生表的联接)(我知道这对于Microsoft SQL Server是正确的,但在使用不同的后端时测试建议)。

索引,索引,索引。并更新这些统计信息(如果适用于您的数据库)。

使查询成为可搜索的。这意味着避免了一些不可能使用索引的事情,比如在like子句的第一个字符中使用通配符,或者在join中使用函数,或者作为where语句的左边部分。

使用正确的数据类型。在日期字段上进行日期数学计算比尝试将字符串数据类型转换为日期数据类型快,然后再进行计算。

千万不要在触发器中放入任何类型的循环!

大多数数据库都有一种检查查询执行方式的方法。在Microsoft SQL Server中,这称为执行计划。首先检查这些问题,看看问题所在。

在确定需要优化的内容时,请考虑查询的运行频率以及运行时间。有时,对一个一天运行数百万次的查询进行细微的调整,可以获得比清除一个每月只运行一次的长时间运行的查询所需的时间更高的性能。

使用某种类型的探查器工具来找出到底是从数据库发送什么。我记得过去有一次,当存储过程很快并且通过分析发现网页多次而不是一次请求查询时,我们无法理解为什么页面加载速度如此之慢。

探查器还将帮助您找到阻止谁的人。由于来自其他查询的锁,一些单独运行时执行速度很快的查询可能会变得非常慢。


今天最重要的限制因素是有限的内存带宽。多核只是让情况变得更糟,因为核心之间共享带宽。此外,专用于实现高速缓存的有限芯片区域也被划分在核心和线程之间,使这个问题更加恶化。最后,芯片间保持不同缓存一致所需的信令也随着内核数量的增加而增加。这也增加了处罚。

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

很多评论已经提到了缓存友好代码。至少有两种不同的口味:

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

第一个问题特别是与使您的数据访问模式更规则有关,允许硬件预取器高效工作。避免动态内存分配在内存中分散数据对象。使用线性容器而不是链接列表、哈希和树。

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

将数据打包得更紧密,并确保在热循环中使用缓存线中的所有数据,将有助于避免这些其他影响,并允许在缓存中拟合更有用的数据。


  • 你在运行什么硬件?您是否可以使用平台特定的优化(如矢量化)?
  • 你能得到一个更好的编译器吗?例如,从GCC切换到Intel?
  • 你能让你的算法并行运行吗?
  • 可以通过重新组织数据来减少缓存未命中吗?
  • 你能禁用断言吗?
  • 为您的编译器和平台进行微优化。用"在if/else中,将最常见的语句放在第一位"的样式


您可能应该考虑"Google透视图",即确定应用程序在很大程度上是如何并行和并发的,这也不可避免地意味着在某一点上要考虑将应用程序分布在不同的机器和网络上,以便它可以理想地与您所投入的硬件几乎成线性扩展。它。

另一方面,谷歌人也以投入大量人力和资源来解决他们正在使用的项目、工具和基础设施中的一些问题而闻名,例如,通过一个专门的工程师团队对GCC内部进行黑客攻击,为GCC的整个程序优化做准备,从而为Google典型的用例sc做好准备。埃纳里奥斯

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


虽然我喜欢迈克·邓拉维的回答,但事实上,它的确是一个很好的答案,有了支持性的例子,我认为可以非常简单地表达出来:

首先找出最需要时间的事情,并了解原因。

这是一个时间消耗的识别过程,可以帮助您了解必须在哪里改进算法。这是我能找到的唯一一个全方位的语言不可知论的答案,这个问题本应得到充分的优化。同时假设你想在追求速度的过程中独立于体系结构。

因此,虽然该算法可能是优化的,但它的实现可能不是。标识允许您知道哪个部分是哪个:算法或实现。所以,无论哪个时间最长的人都是你的主要候选人。但既然你说你想挤出最后几个百分点,你可能还想检查较小的部分,那些你一开始没有仔细检查过的部分。

最后,对不同的方法实现相同的解决方案或可能不同的算法的性能数据进行一些尝试和错误,可以带来有助于识别浪费时间和节省时间的见解。

HPH,就这样吧。


  • 内联例程(消除调用/返回和参数推送)
  • 尝试通过查表消除测试/开关(如果速度更快)
  • 将循环(达夫的设备)展开到刚好适合CPU缓存的位置
  • 本地化内存访问,以免损坏缓存
  • 如果优化器还没有进行本地化相关计算
  • 如果优化器还没有这样做,则消除循环不变量


  • 当你达到使用高效算法的目的时,问题是你需要什么样的速度或内存。使用缓存在内存中"支付"以获得更高的速度,或者使用计算来减少内存占用。
  • 如果可能(而且更具成本效益),将硬件投入到问题中——更快的CPU、更多的内存或HD可以更快地解决问题,然后尝试对其进行编码。
  • 尽可能使用并行化-在多个线程上运行部分代码。
  • 使用正确的工具进行作业。一些编程语言创建更高效的代码,使用托管代码(即Java/NET)加速开发,但本机编程语言创建更快的运行代码。
  • 微观优化。只有在适用的情况下,才能使用优化的程序集来加快代码的速度,在正确的位置使用SSE/向量优化可以大大提高性能。

分而治之

如果正在处理的数据集太大,则循环遍历其中的块。如果您已经正确地完成了代码,那么实现应该很容易。如果你有一个整体的程序,现在你知道更好了。


首先,正如前面几个答案中提到的,了解什么影响了您的性能——是内存、处理器、网络、数据库还是其他什么东西。根据这一点…

  • …如果是记忆-找一本克努斯很久以前写的书,"计算机编程艺术"系列之一。很有可能是关于排序和搜索的问题——如果我的内存有问题,那么你就必须找出他在哪些地方谈论如何处理缓慢的磁带数据存储。将他的内存/磁带对分别转换为缓存/主内存对(或L1/L2缓存对)。学习他描述的所有技巧——如果你找不到解决你问题的方法,那么请专业的计算机科学家进行专业研究。如果您的内存问题是由于使用了FFT(缓存在执行基数2蝴蝶运算时会错过位反转索引),那么不要雇佣科学家-而是手动逐个优化传递,直到您胜出或到达死胡同。你提到挤出到最后几个百分点,对吗?如果真的很少,你很可能会赢。

  • …如果是处理器-切换到汇编语言。研究处理器规格——什么需要Ticks、VLIW、SIMD。函数调用很可能是可替换的勾选器。学习循环转换-管道,展开。乘法和除法可以用位移位替换/内插(用小整数进行乘法可以用加法替换)。尝试使用较短数据的技巧-如果幸运的话,一条64位的指令可能会被32位的2位指令取代,甚至是16位的4位指令或者8位的8位指令取代。还可以尝试更长的数据——例如,在特定的处理器上,浮点计算可能比双浮点计算慢。如果你有三角函数的东西,用预先计算好的表格来对付它;还要记住,如果精度损失在允许的范围内,小值的正弦值可能会被这个值所取代。

  • …如果是网络-考虑压缩传递给它的数据。用二进制替换XML传输。研究方案。如果您可以处理数据丢失,请尝试使用UDP而不是TCP。

  • …如果是数据库,那就去任何数据库论坛征求意见。内存数据网格、优化查询计划等。

Hth:)


缓存!一种(在程序员的努力中)使几乎任何事情都更快的廉价方法是向程序的任何数据移动区域添加一个缓存抽象层。不管是I/O还是只是传递/创建对象或结构。通常,向工厂类和读写器添加缓存很容易。

有时候缓存不会给你带来太多好处,但只需在所有地方添加缓存,然后在不起作用的地方禁用它,这是一种简单的方法。我经常发现这可以在不需要对代码进行微观分析的情况下获得巨大的性能。


我想这已经用另一种方式说了。但是,当您处理一个处理器密集型算法时,您应该以牺牲其他一切为代价,简化最内部循环中的所有内容。

这对某些人来说似乎很明显,但不管我用什么语言工作,这都是我努力关注的问题。例如,如果您处理的是嵌套循环,并且您发现了一个将一些代码降低一个级别的机会,那么在某些情况下,您可以极大地加快代码的速度。作为另一个例子,有一些小事情需要考虑,比如尽可能使用整数而不是浮点变量,尽可能使用乘法而不是除法。同样,这些都是你最内部的循环应该考虑的事情。

有时,您可能会发现在内部循环中对整数执行数学运算的好处,然后将其缩小到一个浮点变量,您可以在后面使用它。这是一个牺牲一部分速度来提高另一部分速度的例子,但在某些情况下,回报是值得的。


最后几个百分比是非常依赖于CPU和应用程序的…

  • 缓存结构不同,有些芯片具有片上RAM您可以直接映射,ARM(有时)有一个向量单位,SH4是一个有用的矩阵操作码。有GPU吗-也许是一个材质球。TMS320非常对循环中的分支敏感(因此将循环和如有可能,将条件移出室外)。

名单还在继续……但这些东西确实是最后的办法…

为x86构建,并根据代码运行valgrind/cachegrind为了正确的性能分析。或德州仪器公司CCSTUDIO有一个很好的探查器。那你就知道在哪儿了关注…


很难给出这个问题的一般答案。它实际上取决于您的问题域和技术实现。一种相当中性的通用技术:识别不能消除的代码热点,手工优化汇编程序代码。


不像以前的答案那样深奥或复杂,但下面是:(以上为初级/中级)

  • 明显:干燥
  • 向后运行循环,以便始终与0进行比较,而不是与变量进行比较
  • 尽可能使用位运算符
  • 将重复的代码分解为模块/函数
  • 缓存对象
  • 局部变量具有轻微的性能优势
  • 尽可能限制字符串操作


Did you know that a CAT6 cable is capable of 10x better shielding off extrenal inteferences than a default Cat5e UTP cable?

对于任何非离线项目,虽然拥有最好的软件和硬件,但如果您的直通输出很弱,那么这条细线将挤压数据并给您带来延迟,尽管以毫秒计……但如果你说的是最后一滴,那就是得到了一些滴,24/7的任何发送或接收包。


我花了一些时间优化在低带宽和长延迟网络(如卫星、远程、离岸)上运行的客户机/服务器业务系统,并通过相当可重复的过程实现了一些显著的性能改进。好的。

  • 措施:首先了解网络的底层容量和拓扑结构。与业务中的相关网络人员交谈,并利用诸如ping和traceroute之类的基本工具,在典型的操作期间(至少)确定每个客户机位置的网络延迟。接下来,对显示问题症状的特定最终用户函数进行精确的时间测量。记录所有这些测量值及其位置、日期和时间。考虑在客户端应用程序中构建最终用户"网络性能测试"功能,允许超级用户参与改进过程;当您处理因系统性能不佳而受挫的用户时,这样授权他们可能会产生巨大的心理影响。好的。

  • 分析:使用任何和所有可用的日志记录方法,准确地确定在执行受影响的操作期间正在传输和接收的数据。理想情况下,应用程序可以捕获客户机和服务器发送和接收的数据。如果这些包括时间戳,甚至更好。如果没有足够的日志记录(例如,关闭的系统或无法将修改部署到生产环境中),请使用网络嗅探器并确保您真正了解网络级别的情况。好的。

  • 缓存:查找静态或不经常更改的数据重复传输的情况,并考虑适当的缓存策略。典型的例子包括"pick list"值或其他"reference entity",在某些业务应用程序中,这些值可能非常大。在许多情况下,用户可以接受必须重新启动或刷新应用程序才能更新不经常更新的数据,特别是当它可以从常用的用户界面元素的显示中节省大量时间时。确保您了解已经部署的缓存元素的真实行为-许多常见的缓存方法(如HTTP ETag)仍然需要网络往返以确保一致性,并且在网络延迟代价高昂的情况下,您可以使用不同的缓存方法完全避免这种情况。好的。

  • 并行化:寻找逻辑上不需要严格按顺序发布的连续事务,并重新构建系统以并行发布它们。我处理过一个案例,其中一个端到端请求的固有网络延迟为~2秒,这对于单个事务来说不是问题,但是当用户重新控制客户端应用程序之前需要6次连续的2秒往返时,这就成了一个巨大的挫折源。发现这些事务实际上是独立的,允许它们并行执行,从而将最终用户的延迟减少到非常接近于一次往返的成本。好的。

  • 合并:在必须按顺序执行连续请求的地方,寻找机会将它们合并成一个更全面的请求。典型示例包括创建新实体,然后请求将这些实体与其他现有实体关联。好的。

  • 压缩:寻找利用有效负载压缩的机会,要么用二进制形式替换文本形式,要么使用实际的压缩技术。许多现代(即十年内)技术栈几乎透明地支持这一点,因此请确保对其进行了配置。压缩的显著影响常常让我感到惊讶,因为很明显,问题根本上是延迟而不是带宽,在发现这一事实之后,它允许事务放入单个数据包中,或者以其他方式避免数据包丢失,从而对性能产生巨大影响。好的。

  • 重复:回到开始,重新测量您的操作(在相同的位置和时间),并在适当的地方进行改进,记录和报告您的结果。正如所有的优化一样,一些问题可能已经解决,暴露出现在占主导地位的其他问题。好的。

在上面的步骤中,我将重点关注与应用程序相关的优化过程,但是当然,您必须确保底层网络本身以最有效的方式配置,以支持您的应用程序。让业务中的网络专家参与进来,确定他们是否能够应用容量改进、QoS、网络压缩或其他技术来解决问题。通常,他们不会理解您的应用程序的需求,因此(在分析步骤之后)您有能力与他们讨论它是很重要的,而且还要为您要求他们承担的任何成本提供商业理由。我曾经遇到过这样的情况:错误的网络配置导致应用程序数据通过慢速卫星链路而不是陆路链路传输,这仅仅是因为它使用的TCP端口不是网络专家所熟知的;显然,纠正这样的问题会对性能产生巨大影响,因为根本不需要软件代码或配置更改。好的。好啊。


添加这个答案,因为我没有看到它包含在所有其他答案中。

最小化类型和符号之间的隐式转换:

这至少适用于C/C++,即使你已经认为你不需要转换——有时它可以在需要性能的函数周围添加编译器警告,尤其是注意循环内的转换。

gcc spesific:您可以通过在代码周围添加一些详细的pragma来测试这一点,

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifdef __GNUC__
#  pragma GCC diagnostic push
#  pragma GCC diagnostic error"-Wsign-conversion"
#  pragma GCC diagnostic error"-Wdouble-promotion"
#  pragma GCC diagnostic error"-Wsign-compare"
#  pragma GCC diagnostic error"-Wconversion"
#endif

/* your code */

#ifdef __GNUC__
#  pragma GCC diagnostic pop
#endif

我看到过这样的情况,通过减少像这样的警告所引起的转换,可以使速度提高百分之几。

在某些情况下,我有一个头部带有严格的警告,我会将其包含在内以防止意外转换,但是这是一种权衡,因为您可能最终会添加大量的强制转换来进行安静的有意转换,这可能只会使代码更加混乱,从而获得最小的收益。


谷歌的方式是"缓存它"。尽可能不要触摸磁盘"


不可能说。这取决于代码的外观。如果我们可以假设代码已经存在,那么我们可以简单地查看它并从中找出优化它的方法。

更好的缓存位置,循环展开,尽量消除长的依赖链,以获得更好的指令级并行性。在可能的情况下,优先选择条件移动而不是分支。尽可能利用SIMD指令。

了解您的代码在做什么,并了解它运行的硬件。然后,确定需要做什么来提高代码性能变得相当简单。这确实是我能想到的唯一一条真正通用的建议。

好吧,那,还有"在so上显示代码,并为特定的代码请求优化建议"。


这里有一些我使用的快速和肮脏的优化技术。我认为这是"第一次通过"优化。

了解花在哪里的时间,找出到底是什么在花时间。是文件IO吗?是CPU时间吗?是网络吗?是数据库吗?如果这不是瓶颈,那么优化IO是没用的。

了解您的环境知道在哪里进行优化通常取决于开发环境。例如,在VB6中,通过引用比通过值慢,但是在C和C++中,引用速度要快得多。在C语言中,如果返回代码指示失败,那么尝试一些事情并做一些不同的事情是合理的,而在DOT NET中,捕获异常要比在尝试之前检查有效条件慢得多。

索引在经常查询的数据库字段上生成索引。你几乎可以用空间换速度。

避免在要优化的循环内查找,我避免了任何查找。在循环外部查找偏移量和/或索引,然后在循环内部重用数据。

尽量减少IO尝试以减少必须读或写的次数的方式进行设计,尤其是在网络连接上。

减少抽象:代码需要处理的抽象层越多,速度就越慢。在关键循环中,减少抽象(例如,显示避免额外代码的低级方法)

为具有用户界面的项目生成线程,生成新的线程以执行较慢的任务,使应用程序感觉更具响应性,尽管不是。

预处理通常可以用空间换速度。如果存在计算或其他密集的操作,请查看在进入关键循环之前是否可以预计算一些信息。


如果更好的硬件是一个选择,那么一定要去做。否则

  • 检查您是否使用了最佳的编译器和链接器选项。
  • 如果不同库中的热点例程指向频繁调用方,请考虑将其移动或克隆到调用方模块。消除了一些调用开销,并可能提高缓存命中率(参见AIX如何静态地将strcpy()链接到单独链接的共享对象中)。当然,这也会降低缓存命中率,这就是为什么要测量缓存命中率的原因之一。
  • 查看是否有可能使用热点例程的专门版本。缺点是需要维护多个版本。
  • 看看装配工。如果您认为它可能更好,请考虑编译器为什么没有弄明白这一点,以及您如何帮助编译器。
  • 考虑:你真的在使用最好的算法吗?它是适合您输入大小的最佳算法吗?


如果你有很多高度并行的浮点运算,特别是单精度运算,那么试着用opencl或(对于Nvidia芯片)CUDA将其卸载到图形处理器(如果有)。GPU在其着色器中具有巨大的浮点计算能力,这比CPU的计算能力大得多。


通过引用而不是通过值


有时更改数据的布局会有所帮助。在C中,您可以从一个或多个数组结构切换到一个或多个数组结构,反之亦然。


调整操作系统和框架。

这听起来有点过分,但要这样想:操作系统和框架的设计目的是做很多事情。您的应用程序只做非常具体的事情。如果你能让OS正确地处理你的应用程序需要什么,让你的应用程序理解框架(PHP,.NET,Java)是如何工作的,那么你可以从硬件中获得更好的效果。

例如,Facebook在Linux中更改了一些内核级的东西,更改了memcached的工作方式(例如,他们编写了memcached代理,并使用了udp而不是tcp)。

另一个例子是Window2008。Win2K8有一个版本,您可以只安装运行X应用程序(例如Web应用程序、服务器应用程序)所需的基本操作系统。这减少了操作系统在运行进程时的大部分开销,并提供了更好的性能。

当然,你应该在第一步投入更多的硬件…


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

如果在特定的体系结构中,变量大小大于单词大小,那么它会对代码大小和速度产生显著影响。例如,如果您有一个16位的系统,并且经常使用一个long int变量,然后意识到它永远不会超出范围(?32.768…32.767)考虑将其减少到EDOCX1[1]

从我个人的经验来看,如果一个程序已经准备好或者几乎准备好了,但是我们意识到它占用了目标硬件程序内存的110%或者120%,那么变量的快速规范化通常比没有解决问题的频率更高。

此时,优化算法或部分代码本身可能会变得徒劳无功,令人沮丧:

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

许多人犯了这样的错误:他们的变量time精确地存储了毫秒数,即使只有50毫秒的时间步数是相关的。如果你的变量每增加一个就代表50毫秒,那么你就可以把它放入一个更小或等于单词大小的变量中。例如,在一个8位系统中,即使简单地加上两个32位变量,也会生成相当数量的代码,特别是在寄存器数量少的情况下,而8位的加起来又小又快。


在具有模板(C++/D)的语言中,可以尝试通过模板ARG传播常量值。你甚至可以用一个开关来处理一组很小的非常量值。

1
Foo(i, j); // i always in 0-4.

变成

1
2
3
4
5
6
7
8
switch(i)
{
    case 0: Foo<0>(j); break;
    case 1: Foo<1>(j); break;
    case 2: Foo<2>(j); break;
    case 3: Foo<3>(j); break;
    case 4: Foo<4>(j); break;
}

缺点是缓存压力,所以这只会在深度或长时间运行的调用树中获得收益,其中值在持续时间内保持不变。


没有这种笼统的说法可能,这取决于问题域。一些可能性:

由于您没有直接指定应用程序100%计算:

  • 搜索该块的调用(数据库、网络硬盘、显示更新),然后隔离它们和/或将它们放入线程中。

如果您使用的是Microsoft SQL Server数据库:

  • 研究nolock和rowlock指令。(本论坛有主题。)

如果你的应用是纯计算的,你可以看看我的这个关于旋转大图像缓存优化的问题。速度的提高使我震惊。

这是一个长期的尝试,但也许它给出了一个想法,特别是如果你的问题是在成像领域:在代码中旋转位图

另一种方法是尽可能避免动态内存分配。一次分配多个结构,一次释放它们。

否则,请确定最紧的循环,并将它们发布在这里,不管是伪循环还是非伪循环,其中包含一些数据结构。