性能优化策略的最后手段

这个网站上有很多性能问题,但是我发现几乎所有的问题都是针对特定问题和相当狭窄的。 几乎所有的重复建议,以避免过早优化。

我们假设:

  • 代码已经正常工作
  • 所选算法对于问题的情况已经是最优的
  • 代码已被测量,并且违规程序已被隔离
  • 所有优化的尝试也将被测量,以确保它们不会让事情变得更糟
  • 我在这里寻找的是一些策略和技巧,当没有其他任何事情可以做但是无论如何需要的时候,在关键算法中挤出最后的几个百分比。

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

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


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

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

  • 现在最大的接受者是更多的名单建设。 作为一个百分比,之前并没有那么大,但现在是因为更大的问题被删除了。 我找到了一种加快速度的方法,时间下降到17秒。

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

  • 现在我似乎碰壁了。 样品告诉我它到底在做什么,但我似乎无法找到任何可以改进的地方。 然后,我会回顾一下程序的基本设计,以及它的交易驱动结构,并询问它所做的所有列表搜索是否实际上都是由问题的要求来规定的。

    然后我重新进行了一次重新设计,其中程序代码实际上是通过一组较小的源代码生成的(通过预处理器宏),并且程序员并不是经常搞清楚程序员知道的事情是否可预测。 换句话说,不要“解释”要做的事情的顺序,“编译”它。

  • 重新设计完成后,将源代码缩小4倍,时间缩短为10秒。
  • 现在,因为它变得如此之快,所以很难抽样,所以我给它做了10倍的工作,但以下时间是基于原始工作量的。

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

  • 现在一个很大的接受者是我一直在做的诊断印刷。 冲洗 - 4秒。

  • 现在最大的接受者是对malloc的免费调用。 回收对象 - 2.6秒。

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

  • 总加速因数:43.6

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

    PS可能会奇怪为什么我没有使用分析器。 答案是几乎每个这些“问题”都是一个函数调用站点,这些堆栈示例是精确定位的。 即使在今天,Profiler也只是几乎没有想到声明和调用指令比定位更重要,并且比整个函数更容易修复。 实际上,我建立了一个分析器来做到这一点,但是对于代码正在做的一个真正的肮脏亲密来说,没有任何东西可以替代你的手指。 这不是一个问题,因为样本数量很少,因为没有发现的问题太小而容易漏掉。

    补充: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上找到,文件为9311.zip,文件为slug.asc和slug.zip。

    编辑2011/11/26:现在有一个sourceforge项目包含了Visual C ++中的源代码,并详细介绍了它是如何调整的。 它仅经过上述场景的前半部分,并且不遵循完全相同的序列,但仍然得到2-3个数量级的加速。


    建议:

  • 预先计算而不是重新计算 :任何包含具有相对有限输入范围的计算的循环或重复调用都应考虑对包含有效范围内的所有值的计算结果进行查找(数组或字典)投入。 然后在算法中使用简单的查找。
    下方:如果实际使用的预计算值很少,这可能会使情况变得更糟,而查找可能会占用大量内存。
  • 不要使用库方法 :大多数库需要被编写为在各种场景下正确运行,并对参数执行空检查等。通过重新实现一种方法,您可能会去掉大量的逻辑并不适用于您正在使用它的确切情况。
    下方:编写额外的代码意味着更多的表面区域的错误。
  • 使用图书馆方法 :与自己相反,语言图书馆是由比你或我更聪明的人编写的; 几率是他们做得更好,更快。 不要自己实现它,除非你真的能够更快地实现它(即:总是衡量!)
  • 作弊 :在某些情况下,虽然您的问题可能存在确切的计算方法,但您可能不需要“确切”,有时候可能“足够好”并且交易速度更快。 问问自己,答案是否超出1%真的很重要吗? 5%? 甚至10%?
    下方:呃......答案并不准确。

  • 当你无法再提高性能时 - 看看你是否能改善感知性能。

    您可能无法更快地制作fooCalc算法,但通常有多种方法可以让您的应用程序看起来更能响应用户。

    几个例子:

  • 预测用户将要求什么,然后开始处理
  • 在他们进来时显示结果,而不是一次结束
  • 准确的进度表
  • 这些不会让你的程序更快,但它可能会让你的用户更快乐。

    链接地址: http://www.djcxy.com/p/2487.html

    上一篇: Performance optimization strategies of last resort

    下一篇: With arrays, why is it the case that a[5] == 5[a]?