如何优化
优化工作流程
在介绍具体细节之前,我们先谈谈优化的一般过程。
优化是一种重构形式。但是,每一步不是改进源代码的某些方面(代码重复,清晰度等),而是可以提高性能的某些方面:降低CPU,内存使用率,延迟等。这种改进通常以可读性为代价。这意味着除了一套全面的单元测试(以确保你的更改没有破坏任何内容)之外,你还需要一套很好的基准测试,以确保您的更改对性能产生预期的影响。你必须能够验证您的更改是否真的在降低CPU。有时候你认为会改善性能的变化实际上会变成零或负变化。在这些情况下,务必确保撤消修改的程序。
源代码中遇到过的最好的评论是什么?- Stack Overflow
//
//亲爱的维护者:
//
//当你完成试图“优化”这个程序,
//并且已经意识到了什么可怕的错误时,
//请增加以下计数器作为给后人的警告:
//
//total_hours_wasted_here = 42
//
你使用的基准测试必须正确,并为代表性工作负载提供可重复的数字。如果单个的运行差异太大,则会使得小的改进更难以发现。你将需要使用benchstat或等效的统计测试,而不能只是用眼睛去看(请注意,使用统计测试无论如何都是一个好主意)。应该记录运行基准测试的步骤,并且应该向存储库提交任何自定义脚本和工具,并提供如何运行它们的说明。要注意需要很长时间才能运行的大型基准测试套件:它会使开发迭代变慢。
还要注意,任何可以测量的东西都可以优化。确保你正在衡量正确的事情。
下一步是决定你正在优化什么。如果目标是改进CPU,那么什么是可接受的速度。你想要将当前的性能提高2倍吗?10倍?你能否说它是“小于时间T的大小为N的问题”?你想减少内存使用量吗?多少钱?对于内存使用情况的变化,可以接受的速度有多慢?你愿意放弃什么来换取较低的空间需求?
优化服务延迟是一个棘手的问题。整本书都是关于如何对Web服务器进行性能测试的。主要问题是:对于单个函数,对于给定的问题规模,性能相当一致。对于webservices,你没有一个单一的性能数字。一个适当的Web服务基准套件将为给定的需求/秒级别提供延迟分布。这篇演讲很好地概述了Gil Tene的一些问题:“如何不去测量延迟” by Gil Tene
TODO:请参阅后面的关于优化Web服务的部分
绩效目标必须具体。你会(几乎)总是能够更快地做出一些事情。优化往往是一个收益递减的游戏。你需要知道何时停止。你要付出多少努力才能完成最后一点工作。你愿意做出这样的代码是多么难以维护?
Dan Luu之前提到的BitFunnel性能评估的演讲显示了一个使用粗略计算来确定目标性能数据是否合理的例子。
TODO:编程珠玑有“Fermi Problems”。从Jeff Dean’s 幻灯片可以了解
对于绿地开发,你不应该把所有的基准和性能数字都留到最后。很容易说“我们稍后会修复”,但如果性能非常重要,那么从一开始就将是一个设计考虑因素。在解决性能问题时所需的任何重大体系结构更改在截止日期前将过于冒险。请注意,在开发过程中,重点应放在合理的程序设计,算法和数据结构上。在更低层次的堆栈优化应该等到开发周期晚些时候才能获得更完整的系统性能视图。你在系统不完整时执行的任何完整系统配置文件都会对完成系统中瓶颈的位置给出偏斜视图。
TODO:如何避免/发现软件写得不好的情况下的“凌迟”。
作为CI的一部分,基准测试是很难的,因为嘈杂的因素,甚至不同的CI盒子,那么很难获取性能指标。一个好的基础是让开发人员运行基准测试(在适当的硬件上)并将其包含在提交消息中,专门用于处理性能问题。对于那些只是提普通补丁的人来说,尽量在代码审查中捕捉性能下降。
TODO:如何跟踪一段时间的性能表现?
编写你可以测试的代码。你可以在较大的系统上执行分析。你可以通过基准测试测试孤立的部分。你需要能够提取并设置足够的环境上下文,以便基准测试足够并具有代表性。
你的目标是什么和目前的表现之间的差异也会让你知道从哪里开始。如果你只需要10%-20%的性能改进,那么可以通过一些实施调整和较小的修复来实现。如果你需要一个10倍或更多的因子,那么用一个左移代替一个乘法不会削减它。这可能会要求你的堆栈上下进行更改。
良好的性能工作需要从系统设计,网络,硬件(CPU,缓存,存储),算法,调整和调试等多个不同层面的知识。在时间和资源有限的情况下,考虑哪个级别能够提供最大的改进:它并不总是算法或程序调优。
一般而言,优化应该从上到下进行。系统级别的优化将比表达级别的影响更大。确保你在适当的水平上解决问题。
本书主要讨论如何减少CPU使用率,减少内存使用量并减少延迟。很高兴指出你很少能做到这三点。也许CPU时间更快,但现在你的程序使用更多的内存。也许你需要减少内存空间,但现在该程序需要更长的时间。
阿姆达尔定律告诉我们要关注瓶颈。如果你将运行时间仅占5%的例程速度提高一倍,那么整个挂钟的速度只有2.5%。另一方面,将80%的时间加速10%的例程将使运行时间提高近8%。配置文件将有助于确定实际花费的时间。
优化时,你想减少CPU必须完成的工作量。Quicksort比气泡排序更快,因为它能以更少的步骤解决相同的问题(排序)。这是一个更高效的算法。你已经减少了CPU完成相同任务所需完成的工作。
像编译器优化一样,程序调优通常只会在整个运行时间中造成一点小小的负担。大的胜利几乎总是来自算法改变或数据结构的改变,这是你的程序组织方式的根本转变。编译器技术有所改进,但速度很慢。Proebsting定律表明,编译器每18 年的性能翻倍,这与摩尔定律(稍微误解了解释)形成鲜明对比,该定律使处理器性能每18 个月翻一番。算法改进在更大的范围内工作。从1991年到2008年,混合器整数规划算法提高了30,000倍 有关更具体的示例,请考虑@buckhx/unwinding-uber-s-most-efficient-service-406413c5871d">此故障取代优步博客文章中描述的蛮力地理空间算法,使用更适合于所提交任务的更专业的算法。没有编译器开关可以提供相同的性能提升。
TODO:在gttse07.pdf中优化浮点FFT和MMM算法的差异
分析器可能会告诉你,大量的时间都花在了特定的例程上。这可能是一个昂贵的例程,或者它可能是一个便宜的例程,只是被称为许多次。而不是立即加快这一过程,看看你是否可以减少被调用的次数或完全消除它。我们将在下一节讨论更具体的优化策略。
三个优化问题:
- 我们必须这样做吗?最快的代码是永远不会运行的代码。
- 如果是的话,这是最好的算法。
- 如果是的话,这是这个算法的最佳实现。