【问题标题】:Performance optimization strategies of last resort [closed]不得已的性能优化策略[关闭]
【发布时间】:2010-10-29 21:39:24
【问题描述】:

这个网站上已经有很多性能问题,但在我看来,几乎所有问题都非常针对特定问题并且相当狭窄。并且几乎所有的人都会重复这些建议以避免过早优化。

假设:

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

我在这里寻找的是策略和技巧,可以在没有其他事情可做的情况下,在关键算法中挤出最后几个百分点。

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

我会在回复中添加我自己的初步建议,并期待 Stack Overflow 社区能想到的任何其他内容。

【问题讨论】:

    标签: performance optimization language-agnostic


    【解决方案1】:

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

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

    • 现在最大的耗时是建立列表。以百分比来说,以前没那么大,但现在是因为更大的问题被排除了。我想办法加快速度,时间降到了 17 秒。

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

    现在我好像碰壁了。样本准确地告诉我它在做什么,但我似乎找不到任何可以改进的地方。然后我反思了程序的基本设计,它的事务驱动结构,并询问它所做的所有列表搜索是否实际上是由问题的要求所要求的。

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

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

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

    • 更多诊断表明它正在花费时间进行队列管理。内联这些将时间减少到 7 秒。

    • 现在,我一直在做的诊断打印需要花费大量时间。冲洗 - 4 秒。

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

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

    总加速因子:43.6

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

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

    我实际上构建了一个分析器来执行此操作,但要真正了解代码正在执行的操作,没有什么可以替代您的手指正确的操作。样本数量少不是问题,因为发现的问题都不是小到容易漏掉的问题。

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

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

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

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

    【讨论】:

    • 我很想阅读您在上面概述的步骤的一些详细信息。是否可以包含一些风味优化的片段? (不要让帖子太长?)
    • ...我还写了一本书,现在已经绝版了,所以它在亚马逊上的价格很可笑——“构建更好的应用程序”ISBN 0442017405。基本上相同的材料在第一章。
    • @Mike Dunlavey,我建议告诉 Google 你已经扫描了它。他们可能已经与购买您的出版商的人达成协议。
    • @Thorbjørn:为了跟进,我确实联系了 GoogleBooks,填写了所有表格,然后给他们发了一份硬拷贝。我收到一封电子邮件,询问我是否真的拥有版权。出版商 Van Nostrand Reinhold 被 International Thompson 收购,又被路透社收购,当我尝试给他们打电话或发电子邮件时,它就像一个黑洞。所以它陷入了困境——我还没有精力去追它。
    【解决方案2】:

    建议:

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

    【讨论】:

    • 预计算并不总是有帮助,有时甚至会造成伤害——如果您的查找表太大,它会影响您的缓存性能。
    • 作弊往往是赢家。我有一个色彩校正过程,其核心是一个点缀着 3x3 矩阵的 3 向量。 CPU 在硬件中有一个矩阵乘法,它省略了一些交叉项,与所有其他方法相比,它的速度非常快,但只支持 4x4 矩阵和 4 个浮点向量。更改代码以携带额外的空槽并将计算从定点转换为浮点,从而获得稍微不太准确但快得多的结果。
    • 作弊在于使用矩阵乘法忽略了一些内积,从而可以在微码中实现单个 CPU 指令,其完成速度甚至比单个指令的等效序列更快。这是一个作弊,因为它没有得到“正确”的答案,只是一个“足够正确”的答案。
    • @RBerteig:在我的经验中,“足够正确”是大多数人错过的优化机会。
    • 你不能总是假设每个人都比你聪明。最后,我们都是专业人士。但是,您可以假设您使用的特定库存在并且由于其质量而已到达您的环境,因此该库的编写必须非常彻底,您不能这样做只是因为您不擅长该库领域,并且您不会在其中投入相同的时间。不是因为你不够聪明。来吧。
    【解决方案3】:

    当您无法再提高性能时 - 看看您是否可以改为提高感知性能。

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

    几个例子:

    • 预测用户的行为 请求并开始工作 在那之前
    • 将结果显示为 他们进来,而不是一下子进来 最后
    • 准确进度表

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

    【讨论】:

    • 进度条最后加速可能被认为比绝对准确的进度条快。在“Rethinking the Progress Bar”(2007 年)中,Harrison、Amento、Kuznetsov 和 Bell 在一组用户身上测试了多种类型的进度条,并讨论了一些重新安排操作的方法,以便人们认为进度更快。
    • naxa,大多数进度条都是假的,因为将流程的多个差异很大的步骤预测为单个百分比是困难的,有时甚至是不可能的。看看那些卡在 99% 的柱子吧:-(
    【解决方案4】:

    我大部分时间都在这个地方度过。大体上是运行你的分析器并让它记录下来:

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

    还有一件我喜欢做的事:

    • 将编译器设置为输出汇编列表并查看它为代码中的热点函数发出的内容。所有那些“一个好的编译器应该能够自动为你做的”聪明的优化?您的实际编译器可能不会这样做。我见过 GCC 发出真正的 WTF 代码。

    【讨论】:

    • 我主要使用 Intel VTune 和 PIX。不知道它们是否可以适应 C#,但实际上,一旦你拥有了 JIT 抽象层,大多数这些优化都超出了你的能力范围,除了提高缓存局部性并可能避免一些分支。
    • 即便如此,检查 JIT 后的输出可能有助于确定是否有任何结构在 JIT 阶段不能很好地优化...调查永远不会受到伤害,即使结果是死胡同。
    • 我想很多人,包括我自己,都会对gcc制作的这个“wtf程序集”感兴趣。你的工作听起来很有趣:)
    • Examples on the PowerPC ... 一些 PowerPC的实现。 PowerPC 是 ISA,而不是 CPU。
    • @BillyONEal 即使在现代 x86 硬件上,imul 也可以停止管道;请参阅“英特尔® 64 和 IA-32 架构优化参考手册”第 13.3.2.3 节:“整数乘法指​​令需要几个周期才能执行。它们是流水线的,因此整数乘法指​​令和另一个长延迟指令可以在执行阶段。但是,由于程序顺序的要求,整数乘法指​​令会阻塞其他单周期整数指令的发出。这就是为什么通常最好使用字对齐数组大小和lea
    【解决方案5】:

    投入更多硬件!

    【讨论】:

    • 如果您的软件预计可以在已经在现场使用的硬件上运行,则并不总是可以选择更多硬件。
    • 对于制作消费软件的人来说,这不是一个很有帮助的答案:客户不会希望听到您说“购买更快的计算机”。特别是如果您正在编写针对视频游戏机之类的软件。
    • @Crashworks,或者就此而言,嵌入式系统。当最后一个功能终于出现并且第一批板已经旋转时,并不是发现您应该首先使用更快的 CPU...
    • 我曾经不得不调试一个存在大量内存泄漏的程序——它的 VM 大小以每小时大约 1Mb 的速度增长。一位同事开玩笑说,我需要做的就是以恒定的速率添加内存。 :)
    • 更多硬件:是的,平庸的开发者的生命线。 “再添一台机器,容量翻倍!”不知道听过多少遍了!
    【解决方案6】:

    更多建议:

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

    • 将 I/O 提前移动:加载您要处理的所有数据 需要预先计算,这样您就不会 在关键的核心内重复 I/O 等待 算法(并且可能导致重复的磁盘寻道,当 一次性加载所有数据可能会避免查找)。

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

    • Threaded I/O:对于那些足够大胆的人,可以结合 'I/O up-front' 或 'Delay I/O' 与实际计算 将加载移动到并行线程中,这样虽然 您正在加载更多数据,您可以进行计算 您已经拥有的数据,或者在您计算下一个数据时 一批数据可以同时写出结果 从最后一批开始。

    【讨论】:

    • 请注意,“将 IO 移动到并行线程”应该在许多平台(例如 Windows NT)上作为异步 IO 完成。
    • I/O 确实是一个关键点,因为它很慢并且有很大的延迟,你可以通过这个建议变得更快,但它仍然存在根本性的缺陷:关键点是延迟(必须被隐藏)和系统调用开销(必须通过减少 I/O 调用的数量来减少)。最好的建议是:使用mmap() 进行输入,进行适当的madvise() 调用并使用aio_write() 编写大块输出(= 几MiB)。
    • 最后一个选项在 Java 中很容易实现,尤其是。它为我编写的应用程序带来了巨大的性能提升。另一个重要的一点(不仅仅是提前移动 I/O)是使其成为 SEQUENTIAL 和大块 I/O。由于磁盘寻道时间的原因,大量小读取比 1 大读取要昂贵得多。
    • 有一次我在避免 I/O 方面作弊,只是在计算之前将所有文件临时移动到 RAM 磁盘,然后再将它们移回。这很脏,但在您无法控制进行 I/O 调用的逻辑的情况下可能很有用。
    【解决方案7】:

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

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

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

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

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

    查询sargable。意思是避免导致无法使用索引的事情,例如在 like 子句的第一个字符或连接中的函数或 where 语句的左侧使用通配符。

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

    切勿将任何类型的循环放入触发器中!

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

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

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

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

    【讨论】:

      【解决方案8】:

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

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

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

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

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

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

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

      【讨论】:

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

        【讨论】:

        【解决方案10】:

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

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

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

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

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

        HPH, asoudmove。

        【讨论】:

          【解决方案11】:

          您可能应该考虑“Google 视角”,即确定您的应用程序如何在很大程度上实现并行化和并发,这也不可避免地意味着在某些时候考虑将您的应用程序分布在不同的机器和网络上,以便理想地实现与您投入的硬件几乎成线性关系。

          另一方面,Google 员工也以投入大量人力和资源来解决他们正在使用的项目、工具和基础架构中的一些问题而闻名,例如 whole program optimization for gcc工程师破解 gcc 内部结构,以便为 Google 的典型用例场景做好准备。

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

          【讨论】:

            【解决方案12】:
            • 内联例程(消除调用/返回和参数推送)
            • 尝试通过查找表来消除测试/切换(如果它们更快)
            • 将循环(Duff 的设备)展开到刚好适合 CPU 缓存的位置
            • 本地化内存访问,以免损坏缓存
            • 如果优化器尚未本地化相关计算
            • 如果优化器尚未这样做,则消除循环不变量

            【讨论】:

            • IIRC Duff 的设备很少更快。仅当运算非常短时(如单个小数学表达式)
            【解决方案13】:
            • 当您使用高效算法时,问题是您需要更多速度还是内存。使用缓存在内存中“付费”以提高速度或使用计算来减少内存占用。
            • 如果可能(并且更具成本效益)解决问题 - 更快的 CPU、更多内存或 HD 可以比尝试编写代码更快地解决问题。
            • 尽可能使用并行化 - 在多个线程上运行部分代码。
            • 为工作使用正确的工具。一些编程语言创建更高效​​的代码,使用托管代码(即 Java/.NET)加速开发,但本机编程语言创建更快的运行代码。
            • 微优化。只有在适用的情况下,您才能使用优化的汇编来加速小段代码,在正确的地方使用 SSE/向量优化可以大大提高性能。

            【讨论】:

              【解决方案14】:

              分而治之

              如果正在处理的数据集太大,循环遍历它的块。如果你的代码正确,实现应该很容易。如果你有一个单体程序,现在你知道得更多了。

              【讨论】:

              • +1 表示我在阅读最后一句话时听到的苍蝇拍“啪”的声音。
              【解决方案15】:

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

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

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

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

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

              HTH :)

              【讨论】:

                【解决方案16】:

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

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

                【讨论】:

                  【解决方案17】:

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

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

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

                  【讨论】:

                    【解决方案18】:

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

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

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

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

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

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

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

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

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

                    【讨论】:

                      【解决方案19】:

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

                      【讨论】:

                        【解决方案20】:

                        最后几个 % 非常依赖于 CPU 和应用程序......

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

                        名单还在继续......但这些事情确实是 最后的办法……

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

                        【讨论】:

                          【解决方案21】:

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

                          对于任何非离线项目,虽然拥有最好的软件和最好的硬件,但如果你的吞吐量很弱,那么细线会挤压数据并给你带来延迟,尽管以毫秒为单位......但如果你在谈论最后一滴,这是获得的一些滴,24/7 发送或接收的任何包裹。

                          【讨论】:

                            【解决方案22】:

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

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

                            【讨论】:

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

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

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

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

                            嗯,那个,以及“在 SO 上显示代码并针对该特定代码寻求优化建议”。

                            【讨论】:

                              【解决方案24】:

                              如果可以选择更好的硬件,那么肯定会选择。否则

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

                              【讨论】:

                              • 我会在您的第一段中添加:不要忘记关闭编译器选项中的所有调试信息
                              【解决方案25】:

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

                              【讨论】:

                                【解决方案26】:

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

                                了解时间都花在了哪些地方 准确了解哪些事情需要花费时间。是文件IO吗?是CPU时间吗?是网络吗?是数据库吗?如果这不是瓶颈,那么优化 IO 是没有用的。

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

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

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

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

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

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

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

                                【讨论】:

                                  【解决方案27】:

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

                                  【讨论】:

                                    【解决方案28】:

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

                                    尽量减少类型和符号之间的隐式转换:

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

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

                                    #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
                                    

                                    我见过一些案例,您可以通过减少此类警告引发的转化来获得几个百分比的加速。

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

                                    【讨论】:

                                    • 这就是我喜欢 OCaml 的原因,数字类型之间的转换必须是 xplicit。
                                    • @Gaius 公平点——但在许多情况下,改变语言并不是一个现实的选择。由于 C/C++ 被如此广泛地使用,因此即使它的编译器特定,它也可以使它们更加严格。
                                    【解决方案29】:

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

                                    【讨论】:

                                      【解决方案30】:

                                      调整操作系统和框架。

                                      这听起来可能有点矫枉过正,但请这样想:操作系统和框架旨在做很多事情。您的应用程序只做非常具体的事情。如果您可以让操作系统完全满足您的应用程序的需求,并让您的应用程序了解框架(php、.net、java)的工作原理,那么您可以更好地利用硬件。

                                      例如,Facebook 在 Linux 中更改了一些 kernel level thingys,更改了 memcached 的工作方式(例如,他们编写了一个 memcached 代理和 used udp instead of tcp)。

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

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

                                      【讨论】:

                                      • 在所有其他方法都失败后,或者如果特定操作系统或框架功能导致性能显着下降,这将是一种有效的方法,但实现这一目标所需的专业知识和控制水平可能不是可用于每个项目。
                                      猜你喜欢
                                      • 2012-06-10
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 2018-08-14
                                      • 1970-01-01
                                      相关资源
                                      最近更新 更多