最后的性能优化策略

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

我们假设:

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

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

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

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

86592 次浏览

向它扔更多的硬件!

很难对这个问题给出一般的答案。这实际上取决于你的问题领域和技术实现。一种与语言无关的通用技术:识别无法消除的代码热点,并手工优化汇编代码。

建议:

  • 预先计算,而不是重新计算:任何包含输入范围相对有限的计算的循环或重复调用,考虑进行查找(数组或字典),其中包含有效输入范围内所有值的计算结果。然后在算法中使用一个简单的查找 缺点:如果实际使用的预计算值很少,这可能会使情况更糟,而且查找可能会占用大量内存。
  • 不要使用标准库方法:大多数库需要在广泛的场景下正确运行,并对参数执行空检查等。通过重新实现一个方法,你可以去掉很多不适用于你正在使用它的确切情况的逻辑 缺点:编写额外的代码意味着更多的bug表面积。
  • 使用库方法:自相矛盾的是,语言库是由比你我聪明得多的人编写的;很可能他们做得更好更快。不要自己实现它,除非你真的可以让它更快(即:总是测量!)
  • 在某些情况下,虽然你的问题可能有一个精确的计算,但你可能不需要“精确”,有时一个近似值可能“足够好”,而且在交易中要快得多。问问你自己,答案相差1%真的重要吗?5% ?甚至10% ?< br > 下行< / em >:嗯……答案并不准确。
  • 你在什么硬件上运行?您是否可以使用特定于平台化的优化(如向量化)?
  • 你能找到更好的编译器吗?比如从GCC换成Intel?
  • 你能让你的算法并行运行吗?
  • 可以通过重新组织数据来减少缓存丢失吗?
  • 可以禁用断言吗?
  • 对编译器和平台进行微优化。在if/else语句中,把最常见的语句放在前面
  • 内联例程(消除调用/返回和参数推送)
  • 试着用表查找(如果它们更快的话)消除测试/开关
  • 展开循环(Duff的设备)到刚好适合CPU缓存的位置
  • 本地化内存访问,以免耗尽缓存
  • 如果优化器还没有本地化相关的计算
  • 如果优化器还没有这样做,就消除循环不变量

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

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

理解你的代码在做什么,理解它运行在什么硬件上。然后,决定需要做什么来提高代码的性能就变得相当简单了。这是我能想到的唯一一个真正具有普遍性的建议。

好吧,还有“在SO上显示代码,并为特定的代码段寻求优化建议”。

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

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

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

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

  • 现在最费时的工作是列清单。从百分比上看,以前没有那么大,但现在是因为更大的问题被消除了。我找到了加快速度的方法,时间缩短到了17秒。

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

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

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

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

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

  • 进一步的诊断表明,它是在队列管理上花费时间的。内联这些将时间缩短到7秒。

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

  • 现在最浪费时间的是调用mallocfree。回收对象- 2.6秒。

  • 继续进行抽样,我仍然发现了严格意义上没有必要的操作——1.1秒。

总加速系数:43.6

现在没有两个程序是相同的,但在非玩具软件中,我总是看到这样的进展。首先你得到简单的东西,然后是更难的东西,直到你达到一个收益递减的点。然后,你获得的洞察力很可能会导致重新设计,开始新一轮的加速,直到你再次遇到收益递减。现在,可能有理由怀疑++ii++for(;;)while(1)哪个更快:我经常在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)

它们使用列表集群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的时间,所以现在是一条“大鱼”。总的来说,就是这样。

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

< p >参考补充道: 源代码,无论是原始的还是重新设计的,都可以在1993年的www.ddj.com中找到,文件9311.zip, files slug。Asc和slug.zip.

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

由于许多性能问题都涉及数据库问题,因此在调优查询和存储过程时,我将介绍一些需要注意的具体问题。

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

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

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

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

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

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

永远不要在触发器中放入任何形式的循环!

大多数数据库都有一种方法来检查如何执行查询。在Microsoft SQL Server中,这被称为执行计划。先检查一下,看看问题出在哪里。

在确定需要优化的内容时,考虑查询运行的频率以及运行所需的时间。有时,对一个每天运行数百万次的查询稍作调整,可以获得比删除一个月只运行一次的long_running查询更多的性能。

使用某种分析器工具来找出发送到数据库和从数据库发送的内容。我记得过去有一次,我们不知道为什么页面加载这么慢,而存储过程却很快,并通过分析发现网页多次而不是一次地请求查询。

剖析器还将帮助您找到谁在阻止谁。一些单独运行时执行很快的查询可能会因为来自其他查询的锁而变得非常慢。

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

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

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

我想这已经用不同的方式说过了。但是当你在处理一个处理器密集型算法时,你应该以牺牲其他所有东西为代价来简化最内部循环中的所有东西。

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

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

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

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

举几个例子:

  • 预期用户将要做什么 请求并开始着手这项工作 在那之前李< / >
  • 显示结果为 它们是进来的,而不是同时出现的 在结尾
  • 准确的进度表

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

我大半辈子都在这里度过。大致的方法是运行你的分析器并记录它:

  • # EYZ0。在大多数程序中,数据缓存是导致死机的首要原因。通过重组违规数据结构以获得更好的局部性来提高缓存命中率;压缩结构和数值类型以消除浪费的字节(因此浪费缓存读取);尽可能地预取数据以减少失速。
  • # EYZ1。编译器对指针混叠的假设,以及数据通过内存在断开连接的寄存器集之间移动的情况,可能会导致某种病态行为,导致整个CPU管道在加载操作时清空。找到浮点数、向量和整型数据相互转换的地方,并消除它们。随意使用__restrict向编译器承诺混淆。
  • # EYZ0。大多数处理器都有一些不能被流水线化的操作,而是运行一个存储在ROM中的小子程序。PowerPC上的例子是整数乘、除和按变量量移位。问题是整个管道在执行此操作时突然停止。尽量避免使用这些操作,或者至少将它们分解为组成它们的流水线操作,这样无论程序的其余部分在做什么,都可以获得超标量分派的好处。
  • # EYZ0。这些也会清空管道。找出CPU在分支结束后花费大量时间重新填充管道的情况,并使用分支提示(如果可用的话)使其更经常地正确预测。或者更好的是,尽可能用条件移动替换分支,在浮点操作之后使用特别是,因为它们的管道通常更深,在fcmp之后读取条件标志可能会导致失速。
  • # EYZ0。制作这些SIMD。

我还喜欢做一件事:

  • 将编译器设置为输出程序集清单并查看它为代码中的热点函数发出了什么。所有那些聪明的优化,“一个好的编译器应该能够自动为你做”?实际的编译器可能不会执行这些操作。我见过GCC发出真正的WTF代码。

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

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

因为你没有直接指定你的应用程序是100%计算:

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

如果你使用的数据库恰好是Microsoft SQL Server:

  • 研究nolock和rowlock指令。(在这个论坛上有一些讨论。)

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

这是一个很长的机会,但它可能提供了一个想法,特别是如果您的问题是在成像域:rotating-bitmaps-in-code

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

否则,请确定最紧密的循环,并将它们与一些数据结构一起张贴在这里(无论是伪的还是非的)。

  • 当你使用高效的算法时,问题是你需要更多的速度或内存。使用缓存来“支付”内存以获得更快的速度,或者使用计算来减少内存占用。
  • 如果可能的话(并且更具成本效益)用硬件来解决问题 -更快的CPU,更多的内存或HD可以更快地解决问题,然后尝试编码。
  • 使用并行化(如果可能的话)——在多个线程上运行部分代码。
  • # EYZ0。一些编程语言创建更有效的代码,使用托管代码(即Java/. net)加快开发,但原生编程语言创建更快运行的代码。
  • # EYZ0。只有在适用的情况下,你才能使用优化的程序集来加快小段代码的速度,在正确的地方使用SSE/矢量优化可以大大提高性能。

更多的建议:

  • 避免I / O:任何I/O(磁盘,网络,端口等)是 总是会比其他代码慢得多 执行计算,所以摆脱你所做的任何I/O

  • 将I/O移到前面:加载你要去的所有数据 需要提前进行计算,这样你就不用了 是否有重复的I/O等待在关键的核心 算法(可能作为结果重复磁盘查找,当

  • .

    .

    .

  • I / O延迟:不要写你的结果,直到 计算结束,将它们存储在一个数据结构中 然后在最后把那份辛苦的工作一气倒出来 李。< / p > < / >

  • 螺纹的I / O:对于那些足够大胆的人,结合'I/O 提前'或'延迟I/O'与实际计算 移动加载到一个并行线程,以便同时 您正在加载更多可以用于计算的数据 你已经拥有的数据,或者在你计算下一个数据的时候 批量数据可以同时写出结果 来自最后一批。

在带有模板的语言(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;
}

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

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

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

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

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

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

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

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

最后几个%是一个非常CPU和应用程序依赖的东西....

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

这个列表在....上但这类事情真的是

.

构建x86,并对代码运行Valgrind/Cachegrind 进行适当的性能分析。或者德州仪器的 CCStudio有一个甜蜜的分析器。然后你就知道在哪里了 专注…< / p >

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

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

调整操作系统和框架。

这听起来可能有点夸张,但可以这样想:操作系统和框架被设计用来做很多事情。您的应用程序只做非常具体的事情。如果你能让操作系统完全满足你的应用程序的需求,并让你的应用程序理解框架(php,.net,java)是如何工作的,你就能从硬件上得到更好的东西。

例如,Facebook改变了Linux中的一些内核级别的东西,改变了memcached的工作方式(例如他们写了一个memcached代理,还有使用udp代替TCP)。

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

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

谷歌方法是一个选项“缓存它..”只要可能,不要碰磁盘。”

分而治之

如果正在处理的数据集太大,则对其中的大块进行循环。如果代码编写正确,实现应该很容易。如果您有一个单片程序,现在您就更清楚了。

虽然我喜欢Mike Dunlavey的回答,但事实上这是一个很好的答案,并且有支持的例子,我认为它可以简单地表达出来:

首先找出哪些事情最耗费时间,并了解原因。

它是时间消耗的识别过程,可以帮助您了解必须在哪里改进算法。这是我能找到的唯一一个全面的语言不可知论答案,这个问题已经被认为是完全优化的。同时假设您希望在追求速度的过程中独立于体系结构。

因此,虽然算法可能被优化了,但它的实现可能没有。标识可以让您知道哪个部分是哪个部分:算法或实现。所以,占用时间最多的就是你审查的首选对象。但是既然你说你想把最后的%挤出来,你可能还想检查一下较小的部分,那些你一开始没有仔细检查过的部分。

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

< p > HPH, asoudmove . < / p >

# EYZ0

对于任何非离线项目,尽管拥有最好的软件和硬件,但如果你的throughoutput很弱,那么这条细线就会挤压数据并给你带来延迟,尽管只有几毫秒……

此外,CAT6电缆的最大吞吐量更高,因为您实际上更有可能收到铜芯电缆,而不是CCA,铜芯包覆铝,这通常出现在所有标准CAT5e电缆中。

如果您面临丢包,丢包,那么提高24/7操作的吞吐量可靠性可以使您所寻找的不同。

对于那些追求家庭/办公室连接可靠性的人来说,(并且愿意对今年的快餐店说不,在年底你可以在那里你可以)从声誉良好的品牌中送给自己the pinnacle of LAN connectivity in the form of CAT7 cable

通过引用而不是通过值传递

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

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

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

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

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

许多人会犯这样的错误,即使用变量准确地存储他们使用该变量的单位的数值:例如,他们的变量time存储精确的毫秒数,即使只有50毫秒的时间步长是相关的。也许如果你的变量代表50毫秒,每增加1,你将能够适应一个更小或等于单词大小的变量。例如,在8位系统上,即使是两个32位变量的简单加法也会生成相当多的代码,特别是如果您的寄存器数量很少,而8位加法既小又快。

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

弄清楚到底什么占用了你的时间。是文件IO吗?是CPU时间吗?是因为网络吗?是数据库吗?如果IO不是瓶颈,优化IO是没有用的。

知道在哪里优化通常取决于开发环境。例如,在VB6中,通过引用传递比通过值传递慢,但是在C和c++中,通过引用传递要快得多。在C语言中,如果返回代码表明失败,尝试一些东西并做一些不同的事情是合理的,而在Dot Net中,捕获异常比尝试前检查有效条件要慢得多。

在频繁查询的数据库字段上建立索引。你几乎总是可以用空间来换取速度。

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

减少输入输出尝试以一种减少您必须读或写的次数的方式进行设计,特别是在网络连接上

代码要通过的抽象层越多,速度就越慢。在关键循环内部,减少抽象(例如,揭示避免额外代码的低级方法)

衍生线程对于带有用户界面的项目,生成一个新线程来执行较慢的任务使应用程序感觉响应更快,尽管它不是。

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

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

  • ...如果是记忆问题,找一本很久以前Knuth写的书,“计算机编程的艺术”系列之一。最有可能是关于排序和搜索的——如果我的记忆是错误的,那么你必须找出他谈论如何处理缓慢的磁带数据存储。在心里把他的内存/磁带对分别转换成你的缓存/主存(或L1/L2缓存)对。研究他描述的所有技巧——如果你找不到解决你问题的方法,那就雇佣专业的计算机科学家来进行专业的研究。如果你的内存问题是偶然的FFT(在做根号为2的蝴蝶时,在位反转索引处缓存丢失),那么不要雇佣科学家——相反,手动优化一个接一个的传递,直到你赢了或者进入死胡同。你提到了挤出最后的百分之几,对吧?如果是few,你很可能会赢。

  • ...如果是处理器,切换到汇编语言。研究处理器规格- 什么需要滴答, VLIW, SIMD。函数调用很可能是可替换的“吃货”。学习循环转换-管道,展开。乘数和除数可以用位移位替换/内插(小整数乘法可以用加法替换)。尝试使用更短的数据——如果你幸运的话,一个64位的指令可能会被两个32位的指令取代,甚至是4位的16位指令,或者8位的8位指令。也尝试更长数据-例如,在特定处理器上,您的浮点数计算可能会比双浮点数计算慢。如果你有三角函数的东西,用预先计算好的表格来解决;还请记住,如果精度损失在允许的范围内,则可以将sin的小值替换为该值。

  • ...如果是网络-考虑压缩你传递的数据。用二进制文件替换XML传输。研究协议。如果你能处理数据丢失,试试UDP而不是TCP。

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

HTH:)

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

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

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

  • Cache:寻找重复传输静态或不经常变化的数据的情况,并考虑适当的缓存策略。典型的例子包括“选择列表”值或其他“引用实体”,在一些业务应用程序中,这些值可能非常大。在许多情况下,用户可以接受必须重新启动或刷新应用程序来更新不经常更新的数据,特别是如果它可以节省大量时间来显示常用的用户界面元素。确保你理解了已经部署的缓存元素的真实行为——许多常用的缓存方法(例如HTTP ETag)仍然需要网络往返来确保一致性,在网络延迟昂贵的地方,你可以用不同的缓存方法来避免它。

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

  • Combine:当连续的请求必须被顺序执行时,寻找机会将它们组合成一个更全面的请求。典型的例子包括创建新实体,然后请求将这些实体与其他现有实体联系起来。

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

  • 重复:回到开始,重新测量你的操作(在相同的地点和时间),并记录和报告你的改进结果。就像所有的优化一样,一些问题可能已经解决了,而另一些问题则暴露了出来。

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

不像之前的答案那么深入或复杂,但下面是: (这些更多是初级/中级水平)

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

如果你有很多高度并行的浮点运算——尤其是单精度运算——尝试使用OpenCL或(对于NVidia芯片)CUDA将其卸载到图形处理器上(如果有的话)。gpu在着色器中拥有强大的浮点计算能力,这比CPU要大得多。

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

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

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

特定于GCC:您可以通过在代码周围添加一些冗长的pragmas来测试这一点,

#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

我曾见过一些案例,你可以通过减少这样的警告所带来的转化率来获得几个百分点的加速。

在某些情况下,我有一个带有严格警告的头,我保留了这些警告,以防止意外转换,然而这是一种权衡,因为您可能最终会为安静的故意转换添加大量强制转换,这可能会使代码更加混乱,而收益却微乎其微。