最后的性能优化策略 [已结束]

Performance optimization strategies of last resort [closed]

提问人: 提问时间:5/29/2009 最后编辑:7 revs, 4 users 65%jerryjvl 更新时间:6/4/2022 访问量:89794

问:


想改进这个问题吗?更新问题,使其仅通过编辑这篇文章来关注一个问题。

10年前关闭。

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

让我们假设:

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

我在这里寻找的是策略和技巧,当除了不惜一切代价之外别无他法时,可以在关键算法中挤出最后百分之几。

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

我将添加一个回复,其中包含我自己的初步建议,并期待 Stack Overflow 社区能想到的任何其他建议。

性能 优化 与语言无关

评论


答:

83赞 sisve 5/29/2009 #1

投入更多硬件!

评论

31赞 Doug T. 5/30/2009
当您的软件有望在现场已经推出的硬件上运行时,更多硬件并不总是一个选择。
77赞 Crashworks 5/30/2009
对于制作消费类软件的人来说,这不是一个非常有用的答案:客户不会想听到你说,“买一台更快的电脑。特别是如果你正在编写针对视频游戏机之类的软件。
20赞 RBerteig 5/30/2009
@Crashworks,或者就此而言,嵌入式系统。当最后一个功能终于出现并且第一批板已经旋转时,并不是发现您首先应该使用更快的 CPU 的时刻......
77赞 j_random_hacker 6/27/2010
我曾经不得不调试一个内存泄漏严重的程序——它的 VM 大小每小时增长约 1Mb。一位同事开玩笑说,我需要做的就是以恒定的速度添加内存。:)
11赞 Olof Forshell 4/1/2011
更多硬件:啊,是的,平庸的开发人员的生命线。不知道听过多少次“再加一台机器,产能翻倍!
7赞 dschwarz 5/29/2009 #2

很难对这个问题给出一个通用的答案。这实际上取决于您的问题领域和技术实现。一种相当中立的通用技术:识别无法消除的代码热点,并手动优化汇编程序代码。

196赞 jerryjvl 5/29/2009 #3

建议:

  • 预计算而不是重新计算:任何包含输入范围相对有限的计算的循环或重复调用,请考虑进行查找(数组或字典),其中包含有效输入范围内所有值的计算结果。然后改用算法内部的简单查找。
    缺点:如果实际使用的预计算值很少,这可能会使情况变得更糟,而且查找可能会占用大量内存。
  • 不要使用库方法:大多数库需要编写才能在广泛的场景下正常运行,并对参数等进行空检查。通过重新实现一个方法,你可以去掉很多在你使用它的确切情况下不适用的逻辑。
    缺点:编写额外的代码意味着 bug 的表面积更大。
  • 使用库方法:自相矛盾的是,语言库是由比你或我聪明得多的人编写的;很有可能他们做得更好更快。不要自己实现它,除非你真的可以让它更快(即:总是测量!
  • 作弊:在某些情况下,尽管您的问题可能存在精确的计算,但您可能不需要“精确”,有时近似值可能“足够好”,并且在交易中要快得多。扪心自问,如果答案是1%,真的重要吗?5%?甚至10%?
    缺点:嗯......答案不会是准确的。

评论

35赞 Adam Rosenfield 5/29/2009
预计算并不总是有帮助的,有时甚至会造成伤害 - 如果你的查找表太大,它可能会降低你的缓存性能。
39赞 RBerteig 5/30/2009
作弊往往是胜利。我有一个色彩校正过程,其核心是一个点缀着 3x3 矩阵的 3 向量。CPU 在硬件中有一个矩阵乘法,它省略了一些交叉项,与所有其他方法相比,速度非常快,但仅支持 4x4 矩阵和 4 向量浮点数。更改代码以携带额外的空槽,并将计算从定点转换为浮点,可以得到稍微不准确但速度更快的结果。
6赞 RBerteig 9/28/2011
作弊是使用矩阵乘法,省略了一些内部积,从而可以在微码中实现单个 CPU 指令,该指令的完成速度甚至比单个指令的等效序列还要快。这是一个作弊,因为它没有得到“正确”的答案,只是一个“足够正确”的答案。
6赞 Martin Thompson 2/23/2013
@RBerteig:“足够正确”是一个优化的机会,根据我的经验,大多数人都错过了。
7赞 v.oddou 7/31/2014
你不能总是假设每个人都比你聪明。最后,我们都是专业人士。但是,您可以假设您使用的特定库存在并且由于其质量而到达您的环境,因此该库的编写必须非常彻底,您不能做得那么好,只是因为您不是该领域的专家,并且您不会投入相同的时间。不是因为你不那么聪明。加油。
26赞 Johan Kotlinski 5/29/2009 #4
  • 您在什么硬件上运行?您可以使用特定于平台的优化(如矢量化)吗?
  • 你能得到一个更好的编译器吗?例如,从 GCC 切换到英特尔?
  • 你能让你的算法并行运行吗?
  • 您能否通过重新组织数据来减少缓存未命中?
  • 你能禁用断言吗?
  • 针对编译器和平台进行微优化。以“在if/else时,将最常见的语句放在第一位”的风格

评论

4赞 Zifre 5/30/2009
应为“从 GCC 切换到 LLVM”:)
4赞 justin 4/30/2011
你能让你的算法并行运行吗?-- 反之亦然
4赞 Johan Kotlinski 4/30/2011
诚然,减少线程数量可能同样是一个很好的优化
0赞 Peter Cordes 11/2/2017
回复:微优化:如果您检查编译器的 ASM 输出,您通常可以调整源代码以手动控制它以产生更好的 ASM。请参阅为什么此 C++ 代码比我手写的程序集更快,用于测试 Collatz 猜想?,了解有关在现代 x86 上帮助或击败编译器的更多信息。
15赞 plinth #5
  • 内联例程(消除调用/返回和参数推送)
  • 尝试通过表格查找来消除测试/切换(如果它们更快)
  • 展开循环(Duff 的设备)到它们刚好适合 CPU 缓存的程度
  • 本地化内存访问,以免破坏缓存
  • 如果优化程序尚未进行本地化,则本地化相关计算
  • 如果优化器尚未这样做,则消除循环不变性

评论

2赞 BCS 6/18/2009
IIRC Duff 的设备很少更快。仅当运算很短时(如单个小数学表达式)
5赞 jalf #6

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

更好的缓存局部性,循环展开,尽量消除长依赖链,以获得更好的指令级并行性。如果可能,首选有条件的移动而不是分支。尽可能利用 SIMD 指令。

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

好吧,那个,然后“在 SO 上显示代码并寻求该特定代码段的优化建议”。

5赞 2 revs, 2 users 84%mealnor #7

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

  • 检查是否使用了最佳编译器和链接器选项。
  • 如果热点例程在不同的库中频繁调用,请考虑将其移动或克隆到调用者模块。消除了一些调用开销,并可能改善缓存命中(参见 AIX 如何将 strcpy() 静态链接到单独链接的共享对象)。这当然也可以减少缓存命中,这就是为什么一个措施。
  • 查看是否有可能使用热点例程的专用版本。缺点是要维护多个版本。
  • 看看汇编程序。如果您认为它可以更好,请考虑为什么编译器没有弄清楚这一点,以及您如何帮助编译器。
  • 想一想:你真的在用最好的算法吗?它是最适合您的输入大小的算法吗?

评论

0赞 varnie 9/5/2013
我想补充你的第一部分:不要忘记关闭编译器选项中的所有调试信息
455赞 19 revs, 5 users 86%Mike Dunlavey #8

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

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

  • 现在,最大的耗时因素是建立列表。作为一个百分比,它以前没有那么大,但现在是因为更大的问题被消除了。我找到了加快速度的方法,时间下降到 17 秒。

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

现在我似乎碰壁了。这些样本确切地告诉我它在做什么,但我似乎找不到任何可以改进的地方。然后,我反思了程序的基本设计,以及它的事务驱动结构,并询问它所做的所有列表搜索是否真的是由问题的需求所强制执行的。

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

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

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

  • 更多的诊断表明,它正在队列管理中花费时间。内联这些将时间缩短到 7 秒。

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

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

  • 继续采样,我仍然发现不是绝对必要的操作 - 1.1 秒。

总加速系数:43.6

现在没有两个程序是一样的,但在非玩具软件中,我总是看到这样的进步。首先你得到简单的东西,然后是更困难的东西,直到你达到回报递减的地步。然后,你获得的洞察力很可能会导致重新设计,开始新一轮的加速,直到你再次遇到递减的回报。现在,这时我们才有理由怀疑 or or or 是否更快:我在 Stack Overflow 上经常看到的问题。++ii++for(;;)while(1)

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

我实际上构建了一个分析器来做到这一点,但要真正深入地了解代码正在做的事情,没有什么可以替代你的手指。样本数量少不是问题,因为发现的问题都不是那么小,以至于很容易被遗漏。

添加: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 的时间,因此现在是一条“更大的鱼”。一般来说,事情就是这样。

我可以补充一点,这个项目是从我帮助的一个真实项目中提炼出来的。在该项目中,性能问题要严重得多(加速也是如此),例如在内部循环中调用数据库访问例程以查看任务是否完成。

添加的参考资料: 源代码,包括原始代码和重新设计的源代码,可以在 1993 年的 www.ddj.com 文件9311.zip、slug.asc 和 slug.zip 中找到。

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

评论

3赞 jerryjvl 5/30/2009
我很想阅读您上面概述的步骤的一些细节。是否可以包括一些风味优化的片段?(不要让帖子太长?
8赞 Mike Dunlavey 5/30/2009
...我还写了一本书,现在已经绝版了,所以它在亚马逊上的价格很荒谬——“构建更好的应用程序”ISBN 0442017405。第一章基本上是相同的材料。
3赞 Thorbjørn Ravn Andersen 4/3/2011
@Mike Dunlavey,我建议告诉谷歌你已经扫描了它。他们可能已经与购买您的发布商的人达成协议。
20赞 Mike Dunlavey 9/5/2011
@Thorbj ørn:为了跟进,我确实联系了GoogleBooks,填写了所有表格,并向他们发送了一份纸质版。我收到一封电子邮件,询问我是否真的拥有版权。出版商Van Nostrand Reinhold,被International Thompson收购,被路透社收购,当我试图给他们打电话或发电子邮件时,它就像一个黑洞。所以它处于不确定状态——我还没有精力真正追逐它。
6赞 Thorbjørn Ravn Andersen 7/16/2014
Google 图书链接: books.google.dk/books?id=8A43E1UFs_YC
48赞 3 revs, 3 users 78%HLGEM #9

由于许多性能问题都涉及数据库问题,因此在优化查询和存储过程时,我将为您提供一些需要注意的具体事项。

在大多数数据库中避免使用游标。也要避免循环。大多数情况下,数据访问应基于设置,而不是逐条记录处理。这包括在要一次插入 1,000,000 条记录时不重复使用单个记录存储过程。

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

避免使用相关的子查询。使用联接(包括尽可能与派生表的联接)(我知道这适用于 Microsoft SQL Server,但在使用其他后端时测试建议)。

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

使查询可优化。含义:避免使用索引的原因,例如在 like 子句的第一个字符中使用通配符,或在 join 中使用函数,或作为 where 语句的左侧部分。

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

永远不要将任何类型的循环放入触发器中!

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

在确定需要优化的内容时,请考虑查询的运行频率以及运行所需的时间。有时,通过对每天运行数百万次的查询进行轻微调整,可以比从每月仅运行一次的long_running查询中擦除时间获得更高的性能。

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

探查器还将帮助您找到谁阻止了谁。某些在单独运行时快速执行的查询可能会由于其他查询的锁定而变得非常慢。

17赞 3 revs, 2 users 75%none #10

你可能应该考虑“谷歌的观点”,即确定你的应用程序如何在很大程度上变得并行化和并发化,这也不可避免地意味着在某个时候考虑将你的应用程序分布在不同的机器和网络上,这样它就可以理想地与你扔给它的硬件几乎线性地扩展。

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

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

8赞 2 revsSteve Wortham #11

我认为这已经以不同的方式说了。但是,当你处理处理器密集型算法时,你应该以牺牲其他一切为代价,简化最内层循环中的所有内容。

这对某些人来说似乎是显而易见的,但无论我使用哪种语言,我都会尝试关注这一点。例如,如果你正在处理嵌套循环,并且你发现有机会将一些代码降低一个级别,那么在某些情况下,你可以大大加快你的代码速度。再举一个例子,有一些小事情需要考虑,比如尽可能使用整数而不是浮点变量,以及尽可能使用乘法而不是除法。同样,这些是应该考虑您最内在循环的事情。

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

176赞 2 revs, 2 users 93%kenj0418 #12

当您无法再提高性能时,请查看是否可以提高感知性能。

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

举几个例子:

  • 预测用户要做什么 请求并开始处理 在此之前
  • 将结果显示为 他们进来了,而不是一下子全部进来 最后
  • 精确的进度计

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

评论

33赞 Emil Vikström 6/26/2012
最后加速的进度条可能被认为比绝对准确的进度条快。在“重新思考进度条”(2007)中,Harrison、Amento、Kuznetsov 和 Bell 在一组用户身上测试了多种类型的进度条,并讨论了一些重新排列操作的方法,以便可以感知进度更快。
11赞 Emil Vikström 6/19/2013
Naxa,大多数进度条都是假的,因为将一个流量的多个截然不同的步骤预测为一个百分比是困难的,有时甚至是不可能的。看看所有那些卡在 99% 的柱线 :-(
146赞 2 revsCrashworks #13

我一生中的大部分时间都在这个地方度过。大致的笔触是运行探查器并让它记录:

  • 缓存未命中。数据缓存是大多数程序中停滞的#1来源。通过重组有问题的数据结构来提高缓存命中率,使其具有更好的局部性;打包结构和数值类型以消除浪费的字节(从而浪费缓存获取);尽可能预取数据以减少停顿。
  • 加载命中存储。编译器对指针混叠的假设,以及数据通过内存在断开连接的寄存器集之间移动的情况,可能会导致某种病态行为,导致整个 CPU 管道在加载操作时清除。找到浮点数、向量和整数相互转换的位置,并消除它们。随意使用以向编译器承诺有关别名。__restrict
  • 微编码操作。大多数处理器都有一些无法流水线化的操作,而是运行存储在 ROM 中的微小子程序。 PowerPC 上的示例包括整数乘法、除法和按变量量移位。问题在于,在执行此操作时,整个管道都会停止。尝试消除这些操作的使用,或者至少将它们分解为它们的组成流水线操作,这样你就可以在程序的其余部分做任何事情时获得超标量调度的好处。
  • 分支预测错误。这些也清空了管道。查找 CPU 在分支后花费大量时间重新填充管道的情况,并使用分支提示(如果可用)使其更频繁地正确预测。或者更好的是,尽可能用条件移动替换分支,尤其是在浮点操作之后,因为它们的管道通常更深,并且在 fcmp 之后读取条件标志可能会导致停顿。
  • 顺序浮点运算。使这些 SIMD.

还有一件事我喜欢做:

  • 将编译器设置为输出程序集列表,并查看它为代码中的热点函数发出的内容。所有那些“一个好的编译器应该能够自动为你做”的聪明优化?你的实际编译器很可能没有做这些事情。我已经看到 GCC 发出真正的 WTF 代码。

评论

8赞 Crashworks 5/31/2009
我主要使用英特尔 VTune 和 PIX。不知道它们是否能适应 C#,但实际上,一旦你有了 JIT 抽象层,这些优化中的大多数都超出了你的能力范围,除了改进缓存局部性,也许避免了一些分支。
6赞 jerryjvl 5/31/2009
即便如此,检查JIT后的输出可能有助于确定是否有任何结构在JIT阶段没有很好地优化......调查永远不会受到伤害,即使结果是死胡同。
6赞 BlueRaja - Danny Pflughoeft 4/29/2011
我想很多人,包括我自己,都会对 gcc 制作的这个“wtf assembly”感兴趣。你的工作听起来非常有趣:)
1赞 Billy ONeal 2/21/2013
Examples on the PowerPC ...<-- 即 PowerPC 的一些实现。PowerPC 是 ISA,而不是 CPU。
1赞 Crashworks 3/1/2013
@BillyONeal 即使在现代 x86 硬件上,imul 也会使管道停止;请参阅“Intel® 64 和 IA-32 架构优化参考手册”§13.3.2.3:“整数乘法指令需要几个周期才能执行。它们通过管道传输,以便整数乘法指令和另一个长延迟指令可以在执行阶段向前推进。但是,由于程序顺序的要求,整数乘法指令会阻止其他单周期整数指令的发出。这就是为什么通常最好使用字对齐的数组大小和 .lea
4赞 Nosredna #14

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

3赞 5 revs, 2 users 77%Marco van de Voort #15

不可能有这样的笼统陈述,这取决于问题域。一些可能性:

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

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

如果您使用了数据库,并且它恰好是 Microsoft SQL Server:

  • 调查 nolock 和 rowlock 指令。(这个论坛上有帖子。

如果您的应用程序纯粹是计算,您可以查看我的这个问题,即旋转大图像的缓存优化。速度的提高让我大吃一惊。

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

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

否则,找出你最紧密的循环,并将它们与一些数据结构一起发布在这里,无论是伪的还是非伪的。

12赞 Dror Helper #16
  • 当您使用高效算法时,您需要什么、更快的速度或内存。使用缓存在内存中“支付”以获得更快的速度,或使用计算来减少内存占用。
  • 如果可能的话(而且更具成本效益),把硬件扔到这个问题上——更快的CPU、更多的内存或硬盘可以更快地解决问题,然后尝试编码。
  • 如果可能,使用并行化 - 在多个线程上运行部分代码。
  • 使用正确的工具完成作业。一些编程语言创建更高效的代码,使用托管代码(即 Java/.NET)可以加快开发速度,但本机编程语言可以创建运行速度更快的代码。
  • 微优化。只有在适用的情况下,您才能使用优化的汇编来加速小段代码,在正确的位置使用 SSE/矢量优化可以大大提高性能。
59赞 2 revs, 2 users 71%Peter Mortensen #17

更多建议:

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

  • 提前移动 I/O:加载要传输的所有数据 需要预先进行计算,这样您就不会 在关键的核心中重复 I/O 等待 算法(也许结果是重复的磁盘搜索,当 一次加载所有数据可能会避免查找)。

  • 延迟 I/O:在 计算结束,将它们存储在数据结构中,然后 然后在最后辛勤工作时一口气把它扔掉 完成。

  • 线程 I/O:对于那些足够大胆的人,将“I/O”组合在一起 up-front“或”延迟 I/O“,实际计算公式为 将加载移动到并行线程中,以便同时 您正在加载更多可以进行计算的数据 您已经拥有的数据,或者在计算下一个数据时 批量数据可以同时写出结果 从最后一批开始。

评论

3赞 Billy ONeal 3/1/2013
请注意,“将 IO 移动到并行线程”应该在许多平台(例如 Windows NT)上作为异步 IO 完成。
2赞 cmaster - reinstate monica 6/29/2013
I/O 确实是一个关键点,因为它很慢,而且有巨大的延迟,你可以通过这个建议来获得更快的速度,但它仍然存在根本性的缺陷:这些点是延迟(必须隐藏)和系统调用开销(必须通过减少 I/O 调用的数量来减少)。最好的建议是:用于输入,进行适当的调用,并用于写入大块输出(= 几 MiB)。mmap()madvise()aio_write()
1赞 BobMcGee 8/27/2013
最后一个选项在 Java 中很容易实现。它为我编写的应用程序提供了巨大的性能提升。另一个重要的点(不仅仅是预先移动 I/O)是使其成为顺序和大块 I/O。 由于磁盘寻道时间的原因,许多小读取比 1 个大读取要昂贵得多。
0赞 Maarten Derickx 4/29/2019
有一次,我在避免 I/O 时作弊,只是在计算之前暂时将所有文件移动到 RAM 磁盘,然后将它们移回。这很脏,但在您无法控制进行 I/O 调用的逻辑的情况下可能很有用。
3赞 2 revs, 2 users 91%BCS #18

在带有模板的语言(C++/D)中,您可以尝试通过模板参数传播常量值。您甚至可以使用开关对一小群不是恒定值的值执行此操作。

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

成为

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;
}

缺点是缓存压力,因此这只会在深度或长时间运行的调用树中增加,其中值在持续时间内是恒定的。

30赞 Mats N #19

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

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

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

  • 避免内存提取延迟。
  • 更低的内存总线压力(带宽)。

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

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

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

7赞 2 revs, 2 users 58%Peter Mortensen #20

最后几个百分点是一个非常依赖 CPU 和应用程序的事情。

  • 缓存架构不同,有些芯片具有片上RAM 你可以直接映射,ARM(有时)有一个向量 单位,SH4 是一个有用的矩阵操作码。有没有 GPU - 也许着色器是要走的路。TMS320 非常 对循环中的分支敏感(因此将循环和 如果可能,将条件移到外面)。

名单还在继续......但这些事情确实是 不得已而为之......

针对 x86 构建,并针对代码运行 Valgrind/Cachegrind 进行适当的性能分析。或者德州仪器 (TI) 的 CCStudio 有一个甜蜜的分析器。然后你就会真正知道在哪里 集中精力...

10赞 2 revs, 2 users 57%Killroy #21

缓存!一种廉价的方法(在程序员的努力下)几乎可以提高速度,方法是将缓存抽象层添加到程序的任何数据移动区域。无论是 I/O 还是只是传递/创建对象或结构。通常,将缓存添加到工厂类和读取器/写入器很容易。

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

4赞 2 revs, 2 users 96%Nir Levy #22

调整操作系统和框架。

这听起来可能有点矫枉过正,但可以这样想:操作系统和框架被设计用来做很多事情。您的应用程序只执行非常具体的操作。如果你能让操作系统完全满足你的应用程序需求,并让你的应用程序了解框架(php、.net、java)是如何工作的,那么你可以从你的硬件中获得更好的效果。

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

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

当然,您应该始终将更多的硬件作为第一步......

评论

3赞 Andrew Neely 7/21/2011
在所有其他方法都失败后,或者如果特定的操作系统或框架功能导致性能显着下降,这将是一个有效的方法,但实现它所需的专业知识和控制水平可能并非每个项目都可用。
5赞 asyncwait #23

谷歌方式是一种选择“缓存它..尽可能不要触摸磁盘”

12赞 MPelletier #24

分而治之

如果正在处理的数据集太大,请遍历其中的块。如果你的代码做对了,实现应该很容易。如果你有一个整体程序,现在你知道得更好了。

评论

9赞 Bryan Boettcher 9/27/2011
+1 代表我在阅读最后一句话时听到的苍蝇拍“啪”的一声。
17赞 2 revsasoundmove #25

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

首先找出花费最多时间的方法,并了解原因。

正是时间猪的识别过程可以帮助您了解必须在哪些方面改进算法。这是我能找到的唯一一个包罗万象的、与语言无关的答案,可以解决一个已经应该完全优化的问题。此外,假设您希望在追求速度的过程中独立于架构。

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

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

高温高压, asoudmove。

7赞 3 revsSåm #26

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

对于任何非离线项目,虽然拥有最好的软件和最好的硬件,但如果你的直通输出很弱,那么这条细线会挤压数据并给你带来延迟,尽管是几毫秒......

此外,CAT6 电缆的最大吞吐量更高,因为您实际上收到的电缆中存在 cupper 芯线的可能性更高,而不是 CCA,即 Cupper 包层铝,这通常是所有标准 CAT5e 电缆中的基础。

I 如果您面临丢包、丢包,那么 24/7 全天候操作的吞吐量可靠性的提高可能会带来您可能正在寻找的差异。

对于那些寻求终极家庭/办公室连接可靠性的人,(并愿意对今年的快餐店说不,在年底你可以在那里你可以)从一个知名品牌那里送自己礼物。the pinnacle of LAN connectivity in the form of CAT7 cable

4赞 Alex Gordon #27

按引用而不是按值传递

评论

4赞 sehe 6/14/2011
虽然这个问题与语言无关,但让我提一下,随着 c++0x 的出现(包括移动语义和扩展的 const rvalue 引用生存期扩展),编译器将(很多时候)能够省略副本(NRVO、URVO),但前提是参数是按值传递的。最终答案:剖析了解您的热点
2赞 underscore_d 2/22/2016
没有用:C++11 撇开(但非常好的观点)不谈,在允许按值传递的语言中,在适当的时候避免它肯定是早期学习阶段的基本口头禅,而不是问题中提出的“最后的优化策略”。
4赞 vsz #28

减小可变尺寸(在嵌入式系统中)

如果变量大小大于特定体系结构上的字长,则可能会对代码大小和速度产生重大影响。例如,如果你有一个 16 位系统,并且经常使用一个变量,后来意识到它永远不会超出范围 (-32.768 ...32.767) 考虑将其简化为long intshort int.

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

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

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

许多人犯了一个错误,即变量恰好存储了他们使用该变量的单位的数值:例如,他们的变量存储了确切的毫秒数,即使只有 50 毫秒的时间步长是相关的。也许如果你的变量表示 50 毫秒,每增加一个 1,你就可以适应一个小于或等于字长的变量。例如,在 8 位系统上,即使简单地添加两个 32 位变量也会生成相当数量的代码,尤其是在寄存器不足的情况下,而 8 位添加既小又快。time

评论

4赞 vsz 10/7/2011
一点也不。不是每个人都使用内存管理的开发系统来开发 Web 应用程序或过度设计的 GUI,这些系统运行在具有千兆字节 RAM 的高端系统上,您可以允许一个小整数使用千兆字节的内存。即使在今天,嵌入式电子设备的整个硬件成本也必须低于 1 美元的系统。对于这样的固件,即使在今天,每个字节都很重要。
2赞 vsz 10/7/2011
我也在嵌入式中工作,我需要一直使用它。也许你在一个有足够大的处理器、布局上有大量空间的领域工作,而且与任务复杂性相比,生产的单元数量非常少,因此开发成本比单位成本更重要。但情况并非总是如此。
2赞 ysap 11/23/2011
我完全和@vsz在一起。很多时候,即使在对中等大小的 DSP 进行编程时,我也会遇到内存问题。这实际上取决于正在实现的特定应用程序。例如,尝试在BlackFin DSP上推送调制解调器软件时,在设计过程的早期就遇到了困难......这迫使您开始聪明地思考,开箱即用地解决内存问题。
3赞 Bryan Boettcher 2/29/2012
这样做要小心。当他们为我的恒温器编写固件时,有人不在,如果我将其保持在“自动”模式(在热和冷之间切换),它会在它击中 -1F 时爆炸交流电,因为有符号 -> 无符号转换。恒温器认为它是 255F :(
1赞 Olof Forshell 3/5/2012
运行托管 Web 服务器应用程序的最先进的服务器库通常会消耗大量电力,因为应用程序一开始就效率低下。也许功耗是启动和运行某些东西的最后筹码。为什么架构目标不应该也针对只需要几台服务器的目标环境呢?这并不难。
5赞 Andrew Neely #29

以下是我使用的一些快速而肮脏的优化技术。我认为这是“第一关”优化。

了解时间花在哪里确切地找出什么需要时间。是文件IO吗?是 CPU 时间吗?是网络吗?是数据库吗?如果这不是瓶颈,那么针对 IO 进行优化是没有用的。

了解您的环境知道在哪里进行优化通常取决于开发环境。例如,在 VB6 中,通过引用传递比按值传递慢,但在 C 和 C++ 中,通过引用传递要快得多。在 C 语言中,如果返回代码指示失败,尝试某些操作并执行不同操作是合理的,而在 Dot Net 中,捕获异常比在尝试之前检查有效条件要慢得多。

指标在经常查询的数据库字段上构建索引。你几乎总是可以用空间换取速度。

避免查找在要优化的循环中,我避免了进行任何查找。找到循环外部的偏移量和/或索引,并重用其中的数据。

最小化 IO:尝试以减少读取或写入次数的方式进行设计,尤其是在网络连接上

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

生成线程 对于具有用户界面的项目,生成一个新线程来执行较慢的任务会使应用程序感觉响应速度更快,尽管事实并非如此。

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

11赞 gnat #30

首先,正如之前的几个答案中提到的,了解是什么影响了你的性能——是内存、处理器、网络、数据库还是其他东西。取决于此...

  • ...如果是记忆 - 找到 Knuth 很久以前写的一本书,这是“计算机编程的艺术”系列之一。最有可能的是,它是关于排序和搜索的 - 如果我的记忆是错误的,那么你必须找出他在其中谈到如何处理缓慢的磁带数据存储。在心理上将他的内存/磁带对分别转换为您的缓存/主存储器对(或 L1/L2 缓存对)。研究他描述的所有技巧 - 如果你没有找到解决你问题的东西,那么聘请专业的计算机科学家进行专业研究。如果你的内存问题是 FFT 偶然造成的(在做 radix-2 蝴蝶时,缓存在位反转索引处未命中),那么不要聘请科学家 - 相反,手动逐个优化传递,直到你要么获胜,要么走到死胡同。你提到挤出最后几个百分点,对吧?如果确实很少,你很可能会赢。

  • ...如果是处理器 - 切换到汇编语言。研究处理器规范 - 需要刻度、VLIW、SIMD 的内容。函数调用很可能是可替换的蜱虫。学习循环转换 - 管道、展开。乘法和除法可以用位移替换/插值(乘以小整数可以用加法替换)。尝试使用较短数据的技巧 - 如果幸运的话,一条 64 位的指令可能会被替换为 2 对 32 或 4 对 16 或 8 对 8 位的指令。也尝试更长的数据 - 例如,在特定处理器上,您的浮点计算可能比双精度计算慢。如果你有三角函数的东西,用预先计算的表格来对抗它;另请记住,如果精度损失在允许的范围内,则小值正弦可能会被替换为该值。

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

  • ...如果是数据库,那么,去任何数据库论坛并寻求建议。内存数据网格,优化查询计划等。

HTH:)

8赞 Pat #31

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

  • 测量:首先了解网络的底层容量和拓扑结构。与业务中的相关网络人员交谈,并利用 ping 和 traceroute 等基本工具,在典型的运营期间(至少)建立每个客户端位置的网络延迟。接下来,对显示问题症状的特定最终用户功能进行准确的时间测量。记录所有这些测量值,以及它们的位置、日期和时间。考虑将最终用户的“网络性能测试”功能构建到客户端应用程序中,允许高级用户参与改进过程;当您与因系统表现不佳而感到沮丧的用户打交道时,像这样赋予他们权力可能会产生巨大的心理影响。

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

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

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

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

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

  • 重复:回到起点,重新衡量您的运营(在相同的地点和时间),并进行改进,记录并报告您的结果。与所有优化一样,一些问题可能已经解决,暴露了现在占主导地位的其他问题。

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

7赞 Aaron #32

不像以前的答案那样深入或复杂,但这里是: (这些是初级/中级水平)

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

评论

5赞 Andreas Reiff 7/11/2013
关于向后循环:是的,循环结束的比较会更快。不过,通常使用该变量将索引到内存中,并且由于频繁的缓存未命中(无预取),反向访问它可能会适得其反。
2赞 underscore_d 2/22/2016
AFAIK,在大多数情况下,任何合理的优化器都可以很好地处理循环,而程序员不必显式反向运行。要么优化器会反转循环本身,要么它有另一种同样好的方式。我注意到(诚然相对简单)循环的 ASM 输出相同,包括升序与最大值和降序与 0。当然,我的 Z80 日子让我习惯了反射性地向后写循环,但我怀疑向新手提及它通常是一种红鲱鱼/过早的优化,当可读代码和学习更重要的实践应该是优先事项时。
0赞 Jack G 2/13/2018
相反,在低级语言中,向后运行循环会更慢,因为在零加加减法与单整数比较之间的战争中,单整数比较更快。您可以在内存中有一个指向起始地址的指针和一个指向内存中结束地址的指针,而不是递减。然后,递增起始指针,直到它等于结束指针。这将消除汇编代码中额外的内存偏移操作,从而证明性能要高得多。
5赞 Demi #33

如果您有很多高度并行的浮点数学运算,尤其是单精度运算,请尝试使用 OpenCL 或(对于 NVidia 芯片)CUDA 将其卸载到图形处理器(如果存在)。GPU 的着色器具有巨大的浮点计算能力,远大于 CPU。

5赞 2 revsideasman42 #34

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

最大程度地减少类型和符号之间的隐式转换:

这至少适用于 C/C++,即使您已经认为自己没有转换 - 有时最好测试在需要性能的函数周围添加编译器警告,尤其是注意循环中的转换。

GCC spesific:您可以通过在代码周围添加一些详细的编译指示来测试这一点,

#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

我见过这样的情况,你可以通过减少这样的警告所引发的转化来获得几个百分点的加速。

在某些情况下,我有一个带有严格警告的标题,我保留这些警告以防止意外转换,但这是一种权衡,因为您最终可能会在安静的有意转换中添加大量强制转换,这可能只会使代码更加混乱以获得最小的收益。

评论

1赞 Gaius 7/25/2014
这就是为什么我喜欢在 OCaml 中,数值类型之间的转换必须是 xplicit。
1赞 ideasman42 8/4/2014
@Gaius公平的观点 - 但在许多情况下,改变语言并不是一个现实的选择。由于 C/C++ 被广泛使用,因此能够使它们更加严格,即使其编译器特定,它也很有用。