程序优化
在计算机科学,程序优化,代码优化或软件优化是修改软件系统以使其某些方面更有效或使用更少的资源的过程。通常,可以优化计算机程序,以使其更快地执行,或者使其能够使用更少的内存存储或其他资源或更少的功率运行。
一般的
尽管“优化”一词与“最佳”具有相同的根源,但优化过程很少产生真正的最佳系统。通常可以使系统成为最佳的,而不是绝对的术语,而只能在给定的质量指标上,这可能与其他可能的指标相反。结果,优化的系统通常仅在一个应用程序或一个受众群体中是最佳的。一个人可能会减少程序以使其消耗更多内存的价格执行某些任务的时间。在内存空间高级的应用程序中,可能会故意选择较慢的算法以使用更少的内存。在所有情况下,通常都没有“一件尺寸适合所有尺寸”的设计,因此工程师会进行权衡以优化最大兴趣的属性。此外,制造一项完全最佳的软件所需的努力(无法进一步改进)几乎总是比获得所产生的好处合理的要多;因此,在达到完全最佳的解决方案之前,可以停止优化过程。幸运的是,通常情况下,最大的改进是在此过程的早期。
即使对于给定的质量指标(例如执行速度),大多数优化方法也只能改善结果;他们没有假装产生最佳输出。超速化是找到真正最佳输出的过程。
优化水平
优化可以在许多级别上进行。通常,较高的级别具有更大的影响,并且以后在项目中很难更改,如果需要更改,则需要进行重大更改或完整的重写。因此,优化通常可以通过从上升到更低的细化来进行,而最初的收益较大,可以通过更少的工作来实现,然后收益较小,需要更多的工作。但是,在某些情况下,总体绩效取决于程序的表现非常低,而在晚期或早期考虑低级细节的小变化可能会产生巨大的影响。通常,在整个项目中都对效率进行了一些考虑 - 尽管这种情况差异很大 - 但通常认为重大优化是迟到的,即使有的话。在长期运行的项目中,通常会有优化的周期,其中改善一个区域会揭示另一个区域的局限性,并且当绩效可接受或收益变得太小或太昂贵时,这些局限性通常会减少。
由于性能是程序规范的一部分 - 一个不可避免地慢的程序不适合目的:可接受60 Hz(每秒框架)的视频游戏是可以接受的,但是每秒6帧的速度是不可接受的波涛汹涌 - 性能是从一开始就考虑的,以确保系统能够提供足够的性能,并且早期的原型需要具有大致可接受的性能,以使最终系统(具有优化)可以实现可接受的性能。有时忽略了这样的信念,即始终可以在以后进行优化,从而导致原型系统太慢 - 通常是按数量级或更多的数量级 - 最终是故障的系统,因为它们在架构上无法实现其绩效目标,例如作为英特尔432 (1981);或需要多年的工作才能实现可接受的绩效的工作,例如Java(1995),仅在Hotspot (1999)中实现了可接受的绩效。绩效在原型和生产系统之间发生变化的程度,以及优化的效果如何,可能是不确定性和风险的重要来源。
设计水平
在最高级别上,可以优化设计以充分利用可用资源,给定目标,约束和预期使用/负载。系统的建筑设计绝大多数会影响其性能。例如,将优化一个网络延迟结合的系统(网络延迟是对整体性能的主要约束),以最大程度地减少网络旅行,理想情况下提出单个请求(或没有请求,例如在推送协议中,而不是多个请求)往返。设计的选择取决于目标:设计编译器时,如果快速编译是关键的优先级,则一通编译器比多通编译器要快(假设相同的工作),但是如果输出代码的速度是目标,则较慢的多通编译器可以更好地实现目标,即使它需要更长的时间。平台和编程语言的选择在此级别上发生,并且经常更改它们需要完整的重写,尽管模块化系统可能只能重写某些组件- 例如,Python程序可以在分布式中重写C的性能-关键性部分。系统,体系结构的选择(客户端服务器,点对点等)发生在设计级别上,并且可能很难更改,特别是如果无法同步替换所有组件(例如,旧客户端)。
算法和数据结构
在总体设计的情况下,接下来是有效的算法和数据结构的理想选择,以及这些算法和数据结构的有效实现。设计后,算法和数据结构的选择比程序的任何其他方面都更大。通常,数据结构比算法更难更改,因为在整个程序中都使用了数据结构假设及其性能假设,尽管可以通过在功能定义中使用抽像数据类型来最大程度地限制这一点,并保持具体数据结构的定义受限制。到几个地方。
对于算法,这主要包括确保算法是恒定的O(1),对数O(log n ),线性O( n ),或者在某些情况下,log-linear O( n log n )在太空中(都在太空中和时间)。具有二次复杂性o( n 2 )的算法无法扩展,甚至线性算法也会反复调用问题,并且如果可能的话,通常会用恒定或对数替换。
除了渐近生长顺序之外,恒定因素很重要:渐近算法速度较慢或较小(因为简单)比渐近算法都面临小输入时,它们可能是现实中发生的情况。由于这种折衷变化,混合算法通常会提供最佳性能。
提高性能的一般技术是避免工作。一个很好的例子是使用快速路径来用于常见案例,通过避免不必要的工作来提高性能。例如,使用用于拉丁文本的简单文本布局算法,仅切换到复杂脚本的复杂布局算法,例如Devanagari 。另一个重要的技术是缓存,尤其是回忆,避免了冗余计算。由于缓存的重要性,系统中通常会有许多级别的缓存级别,这可能会导致记忆使用的问题以及陈旧缓存的正确性问题。
源代码级别
除了一般算法及其在抽像机器上的实现之外,具体源代码级别的选择还可以产生重大不同。例如,在早期C编译器上,while(1)
比慢for(;;)
对于无条件的循环,因为while(1)
评估1,然后进行有条件的跳跃,该跳跃测试是否为真,而for (;;)
有无条件的跳跃。如今,可以通过优化编译器来执行一些优化(例如此)。这取决于源语言,目标机器语言和编译器,并且可能很难理解或预测,并且会随着时间的流逝而变化。在这里,对编译器和机器代码的理解可以提高性能。循环不变的代码运动和返回值优化是优化的示例,可减少对辅助变量的需求,甚至可以通过避免进行圆形的优化来提高性能。
构建水平
在源和编译级别之间,指令和构建标志可以分别用于调整源代码和编译器中的性能选项,例如使用预处理器定义以禁用不需要的软件功能,为特定的处理器模型或硬件功能优化,或预测分支机构,或预测分支机构,或者例如。基于源的软件分销系统(例如BSD的端口和Gentoo的Portage)可以利用这种优化形式。
编译级别
使用优化编译器的使用倾向于确保可执行程序的优化至少与编译器所能预测的一样多。
组装水平
在最低级别上,使用汇编语言编写代码,为特定硬件平台而设计的代码可以产生最有效,最紧凑的代码,如果程序员利用机器说明的完整曲目。因此,许多在嵌入式系统上使用的操作系统一直以汇编代码编写。由于涉及的时间和成本,很少在组装中从头到尾写(非常小的程序)。大多数是从高级语言到组装的,从那里进行了优化。当效率和大小不太重要时,大零件可能会用高级语言编写。
凭借更现代的优化编译器和最近CPU的更复杂性,比编译器生成的编写更有效的代码更难编写,而且很少有项目需要此“最终”优化步骤。
今天编写的大部分代码旨在在尽可能多的机器上运行。结果,程序员和编译器并不总是利用较新的CPU或较旧型号提供的更有效的说明。此外,在不使用此类指令的情况下为特定处理器调整的汇编代码在另一个处理器上仍可能是次优的,期望代码的不同调整。
通常,今天,程序员将使用拆卸器来分析编译器的输出并更改高级源代码,以便更有效地编译它,或者了解为什么它效率低下。
运行
即将到来的编译器可以根据运行时数据生产自定义的机器代码,以汇编开销为代价。该技术可追溯到最早的正则表达引擎,并且与Java Hotspot和V8有关JavaScript的广泛存在。在某些情况下,自适应优化可以通过根据实际输入或其他因素动态调整参数来执行运行时间优化,超过了静态编译器的功能。
配置文件指导的优化是基于运行时间概况的提前(AOT)汇编优化技术,类似于自适应优化动态技术的静态“平均情况”类似物。
自我修改代码可以在运行时间条件下改变自身,以优化代码;这在汇编语言程序中更为普遍。
一些CPU设计可以在运行时执行一些优化。一些示例包括排序执行,投机执行,指令管道和分支预测变量。编译器可以帮助该程序利用这些CPU功能,例如通过说明计划。
依赖平台和独立优化
代码优化也可以大致归类为与平台依赖性和独立于平台的技术。尽管后者在大多数或所有平台上都是有效的,但与平台相关的技术使用一个平台的特定属性,或者根据单个平台甚至单个处理器依赖参数。因此,可能需要为不同处理器编写或制作不同版本的相同代码。例如,在编译级优化的情况下,独立于平台的技术是通用技术(例如循环展开,功能呼叫的降低,内存有效的例程,条件降低等),影响大多数CPU体系结构在类似的CPU体系结构中方式。与内部循环相比,已经显示了与平台无关的优化的一个很好的示例,在该循环中,具有内部循环的循环每单位时间的计算比没有循环或一个内部循环的循环进行更多的计算。通常,这些用来减少完成程序所需的总指令路径长度和/或减少在此过程中的总内存使用情况。另一方面,与平台相关的技术涉及指令计划,指令级并行性,数据级并行性,缓存优化技术(即,在各个平台之间有所不同的参数)以及最佳指令调度可能也可能有所不同。相同的架构。
降低力量
可以以不同的效率以几种不同的方式执行计算任务。具有等效功能的更高效的版本称为强度降低。例如,考虑以下C代码段,其目的是获得1到N的所有整数的总和:
int i, sum = 0;
for (i = 1; i <= N; ++i) {
sum += i;
}
printf("sum: %d\n", sum);
可以使用以下数学公式重写此代码(假设没有算术溢出):
int sum = N * (1 + N) / 2;
printf("sum: %d\n", sum);
优化有时会通过优化编译器自动执行,是选择一种更有效的方法(算法),同时保留相同的功能。有关其中一些技术的讨论,请参见算法效率。但是,通常可以通过删除外部功能来实现绩效的显著改善。
优化并不总是一个明显或直观的过程。在上面的示例中,如果n足够小,“优化”版本实际上可能比原始版本慢,并且特定的硬件在执行添加和循环操作方面的速度比乘法和除法要快得多。
权衡
但是,在某些情况下,优化依赖于使用更多精心设计的算法,利用“特殊情况”和特殊的“技巧”以及进行复杂的权衡。 “完全优化”的程序可能更难理解,因此可能包含比不优化版本更多的故障。除了消除明显的反生态外,某些代码级别的优化可降低可维护性。
优化通常将重点放在提高性能的一个或两个方面:执行时间,内存使用情况,磁盘空间,带宽,功耗或其他资源。这通常需要权衡 - 一个因素以牺牲他人为代价进行优化。例如,增加缓存大小可提高运行时间性能,但也可以增加内存消耗。其他常见的权衡包括代码清晰度和简洁性。
在某些情况下,执行优化的程序员必须决定使某些操作更好,但要使其他操作效率降低。这些权衡有时可能具有非技术性的性质 - 例如,当竞争对手发布基准结果时,必须击败该基准结果以提高商业成功,但可能会带来使软件正常使用效率降低的负担。这种变化有时被开玩笑地称为悲观。
瓶颈
优化可能包括在系统中找到瓶颈- 这是性能的限制因素。在代码方面,这通常是热点- 代码的关键部分,是所需资源的主要消费者 - 尽管它可能是I/O延迟或网络带宽等另一个因素。
在计算机科学中,资源消耗通常遵循一种功率法分配的形式,并且可以通过观察20%的操作通常使用80%的资源来应用帕累托原则。在软件工程中,通常是一个更好的近似值,即计算机程序的执行时间的90%用于执行代码的10%(在这种情况下称为90/10法律)。
更复杂的算法和数据结构在许多项目中的性能都很好,而简单的算法更适合少量数据 - 设置,初始化时间和更复杂的算法的恒定因素可以超过益处,因此是混合算法或适应性算法算法可能比任何单个算法要快。性能剖面师可用于缩小有关哪些功能适合哪些条件的决策。
在某些情况下,添加更多内存可以帮助使程序更快地运行。例如,过滤程序通常会立即读取每条线,并过滤和输出该行。这仅在一行中使用足够的内存,但是由于每个磁盘读取的延迟,性能通常很差。缓存结果同样有效,尽管还需要更大的内存使用。
何时优化
优化可以降低可读性,并添加仅用于提高性能的代码。这可能会使程序或系统复杂化,从而使它们更难维护和调试。结果,通常在开发阶段结束时进行优化或性能调整。
唐纳德·诺斯(Donald Knuth)发表了以下两个有关优化的陈述:
“我们应该忘记小效率,例如大约97%的时间:早产优化是万事万物的根源。但是,我们不应该以3%的关键3%的命中机会来消除我们的机会”
- (几年后,他还将这句话归因于托尼·霍尔(Tony Hoare) ,尽管这可能是一个错误,因为霍尔(Hoare)拒绝了这句话。)
“在既定的工程学科中,很容易获得的12%的改善从未被认为是边缘的,我相信在软件工程中应该占上风的观点”
“过早优化”是一个短语,用于描述程序员让性能考虑影响一件代码的设计的情况。这可能会导致设计不像本来那样干净的设计或不正确的代码,因为该代码因优化而复杂,并且程序员通过优化分散了注意力。
在确定是否优化程序的特定部分时,应始终考虑Amdahl定律:对整个程序的影响很大程度上取决于在该特定部分实际花费了多少时间,从查看代码来看,这并不总是清楚没有性能分析。
因此,一种更好的方法是首先设计,从设计中进行编码,然后对所得代码进行配置/基准测试,以查看应优化哪些零件。在此阶段,简单而优雅的设计通常更容易优化,并且分析可能揭示出意外的性能问题,而这些问题不会通过过早优化解决。
实际上,通常有必要在首次设计软件时牢记性能目标,但是程序员可以平衡设计和优化的目标。
现代编译器和操作系统非常有效,以至于预期的性能提高通常无法实现。例如,在操作系统级别再次缓存的应用程序级别的缓存数据并不能改善执行。即便如此,当程序员将从生产代码中删除失败的优化时,这是一种难得的情况。确实,硬件的进步经常会消除任何潜在的改进,但是,在否定其目的之后,晦涩的代码将持续到未来。
宏
使用宏的代码开发期间的优化采用不同语言的不同形式。
在某些程序语言(例如C和C ++)中,宏是使用令牌替代的。如今,在许多情况下,内联函数可以用作类型的安全替代方案。在这两种情况下,嵌入式功能主体都可以通过编译器进行进一步的编译时间优化,包括恒定折叠,这可能会将某些计算移至编译时间。
在许多功能性编程语言中,使用分析树/抽象语法树的parse时间替换来实现宏,据称这使它们更安全。由于在许多情况下都使用了解释,因此这是确保仅在Parse时间执行此类计算的一种方法,有时是唯一的方法。
LISP起源于这种宏观,这种宏通常被称为“类似LISP的宏”。通过在C ++中使用模板元编程可以实现类似的效果。
在这两种情况下,工作都转移到编译时间。另一侧的C宏与类似LISP的宏和C ++模板元编程之间的差异是,后一种工具允许在编译时/分析时执行任意计算,而C Macros的扩展不执行任何操作计算,并依赖于优化器执行它的能力。此外, C宏不直接支持递归或迭代,因此并不完整。
但是,与任何优化一样,在项目完成之前,通常很难预测此类工具在何处产生的影响最大。
自动化和手动优化
另请参见类别:编译器优化
优化可以由编译器自动化或由程序员执行。收益通常受到局部优化的限制,并且用于全球优化。通常,最强大的优化是找到出色的算法。
通常由程序员进行优化整个系统,因为它对于自动化优化器来说太复杂了。在这种情况下,程序员或系统管理员明确更改代码,以使整体系统的性能更好。尽管它可以产生更好的效率,但比自动优化要贵得多。由于许多参数会影响程序性能,因此程序优化空间很大。荟萃术和机器学习用于解决程序优化的复杂性。
使用Profiler (或性能分析仪)查找占用最多资源的程序的部分 -瓶颈。程序员有时认为他们对瓶颈的位置有清晰的了解,但直觉通常是错误的。优化不重要的代码通常无济于事,可以帮助整体性能。
当瓶颈本地化时,优化通常从对程序中使用的算法进行重新思考开始。通常,特定的算法可以专门针对特定问题量身定制,比通用算法更好的性能。例如,对大量项目进行排序的任务通常是使用QuickSort例程完成的,这是最有效的通用算法之一。但是,如果这些项目的某些特征是可利用的(例如,它们已经按某种特定的顺序排列),则可以使用不同的方法,甚至可以使用定制的排序例程。
在程序员合理地确定选择最佳算法之后,可以启动代码优化。循环可以展开(对于较低的循环上空的头顶,尽管如果CPU缓存超载的速度通常会导致速度较低),那么可以使用尽可能小的数据类型,可以使用整数算术代替浮点,等等,依此类推。 (请参阅有关这些技术和其他技术的算法效率文章。)
性能瓶颈可能是由于语言限制而不是程序中使用的算法或数据结构。有时,程序的关键部分可以用不同的编程语言重写,该语言可以更直接访问基础机器。例如,像Python这样的高级语言通常以更高的速度编写模块是很常见的。已经用C编写的程序可以在汇编中编写模块。 D中编写的程序可以使用内联汇编程序。
在这种情况下,在这种情况下重写部分“有钱”,因为一般的“经验法则”称为90/10法律,该法律规定90%的时间在10%的代码中,只有10%的时间在其余90%的代码中。因此,如果可以找到正确的部分,则将知识分子的努力用于优化程序的一小部分可能会对整个速度产生巨大影响。
手动优化有时具有破坏可读性的副作用。因此,应仔细记录代码优化(最好使用在线注释),并评估其对未来开发的影响。
执行自动优化的程序称为优化器。大多数优化器都嵌入编译器中,并在编译过程中运行。优化器通常可以针对特定处理器定制生成的代码。
如今,自动化优化几乎仅限于编译器优化。但是,由于编译器优化通常仅限于固定的一系列相当通用的优化,因此对优化器有相当大的需求,这些优化器可以接受问题和特定语言优化的描述,从而允许工程师指定自定义优化。接受优化描述的工具称为程序转换系统,并开始应用于C ++等真实软件系统。
一些高级语言( Eiffel , Esterel )通过使用中级语言来优化其程序。
网格计算或分布式计算旨在通过将任务从使用较高的计算机移动到空闲时间的计算机来优化整个系统。
优化花费的时间
有时,在其中进行优化所花费的时间可能是一个问题。
优化现有代码通常不会添加新功能,更糟糕的是,它可能会在先前工作代码中添加新的错误(如任何更改所致)。由于手动优化的代码有时可能比未优化的代码具有更少的“可读性”,因此优化也可能影响其可维护性。优化是有代价的,重要的是要确保投资值得。
自动优化器(或优化编译器,执行代码优化的程序)本身可能必须优化,以进一步提高目标程序的效率,或者加快自己的操作加快。通过“打开”进行优化的汇编通常需要更长的时间,尽管这通常只是程序很大时的问题。