首页 >> 大全

性能优化指南:性能优化的一般性原则与方法

2022-06-13 大全 179 作者:考证青年

作为程序员,性能优化是家常便饭,无论是桌面应用还是网页应用,无论是前端还是后端,无论是单点应用还是分布式系统。本文从以下几个方面考虑这个问题:性能优化的一般原则、性能优化的层次和性能优化的一般方法。本文不限于任何语言、框架,可能会以语言为例。

性能优化指南:性能优化的一般性原则与方法

不过由于个人经验,可能更多是从Linux服务器的角度来考虑这些问题。

一般原则

基于数据,而非猜测

这是性能优化的第一原则。当我们怀疑性能有问题时,我们应该通过测试、记录日志来分析问题出在哪里,并针对它进行定位,而不是靠感觉和运气。如果一个系统出现性能问题,瓶颈可能是 CPU、内存或 IO(磁盘 IO、网络 IO)。对于一般方向,您可以使用 top 和 stat 系列来定位 (,, &;)。可以使用 .

在本文中,主要关注与 CPU 相关的性能问题。根据 80/20 法则,大部分时间都花在了少量的代码片段上。找到这些代码唯一可靠的方法是我所知道的所有编程语言都有相关的工具。熟练使用这些工具就是性能优化。第一步。

避免过早优化

真实的是,他们在错误的时间和错误的时间花费了太多时间;是所有邪恶(或至少大部分)的根源。

我不太清楚 Knuth 名言的背景,但我同意这个想法。在我的工作环境(以及典型的互联网应用开发)和编程模式下,追求的是快速迭代和试错,过早的优化往往是无用的。而且,过早的优化很容易被打败,优化点往往不是真正的性能瓶颈。

避免过度优化

As 是 a 的一部分 – 缓慢的 a 不适合

性能优化的目标是追求合适的性价比。

在不同的阶段,我们会对系统的性能有一定的要求,比如应该达到多少吞吐量。如果达不到这个指标,就需要优化。如果能达到预期,那就不用再花时间精力去优化了。比如只有几十个人使用的内部系统,不需要按照10万在线的目标进行优化。

而且,后面会提到,有些优化方法是“有损”的,可能对代码的可读性和可维护性产生副作用。这时候,就更难过度优化了。

对业务的深刻理解

代码是给企业的,也许是给最终用户的,也许是给其他程序员的。不了解业务,就很难了解系统的流程,也很难发现系统设计的不足。业务理解的重要性在后面也会提到。

性能优化是一场持久战

当核心业务方向明确后,就应该开始关注性能问题。项目上线后,还要继续进行性能测试和优化。

如今的互联网产品已经不是一蹴而就,上线后需要不断开发,用户的大量涌入也会带来性能问题。因此,需要自动检测性能问题,维护稳定的测试环境,不断发现和解决性能问题,而不是被动地等待用户投诉。

选择正确的指标、测试用例、测试环境

因为性能优化是一个长期的行为,所以需要固定测量指标、测试用例、测试环境,从而客观地反映性能的实际情况,展现优化的效果。

衡量性能的指标有很多,比如系统响应时间、系统吞吐量、系统并发度等。不同系统的核心指标不同。首先要明确系统的核心性能需求,固定测试用例;其次,要兼顾其他指标,不能忽视其他指标。

测试环境也很重要。有一次,我们突然发现我们的 QPS 高了很多,但是程序根本没有优化。查了半天,发现我们换了一台更强大的物理机作为测试服务器。

性能优化级别

根据我的理解,可以分为需求阶段、设计阶段、实现阶段;阶段越高,优化效果越明显,对业务和需求的理解越深入。

需求阶段

不战而屈人之兵是好人

程序员的需求可能来自 PM、UI 业务需求(或功能需求),也可能来自团队需求。当我们得到一个需求时,我们首先需要思考和讨论需求的合理性,而不是立即设计和编码。

需求就是解决问题,问题就是本质,需求就是解决问题的手段。所以需求是否真的能解决问题,程序员就得自己去想了。上一篇文章说过,一个产品经理(尤其是懂一点技术的产品经理)的某个需求,可能只是某个问题的解决方案,他认为会解决他的问题,对待解决方案作为一个要求,而不是一个真正的问题。

需求讨论的前提是对业务的深入了解。如果你不了解业务,你根本无法讨论它。即使需求已经实现了,当我们发现有性能问题时,也可以先从需求开始。

需求分析如何帮助性能优化?首先,为了达到同样的目的,解决同样的问题,可能会有性能更好(消耗更少)的方法。这种优化是无损的,即在不改变需求本质的情况下,可以达到性能优化的效果;第二种情况,有损优化,即在不明显影响用户体验的情况下,稍微修改需求,放宽条件。可以大大解决性能问题。PM 后退了一小步,程序向前迈出了一大步。

需求讨论还有助于在设计期间更具可扩展性并应对未来的需求变化,此处未列出。

设计阶段

专家将 80% 的时间用于思考,20% 的时间用于实施;新手写代码非常快,但是有无穷无尽的错误修复

设计的概念非常广泛,包括架构设计、技术选型、界面设计等等。架构设计约束了系统的扩展,技术选型决定了代码实现。编程语言和框架都是工具,不同的系统和业务需要选择合适的工具集。如果设计做得不够好,后期将难以优化,甚至需要推后。

实现阶段

实现是将函数转换为代码的过程。这一层的优化主要是针对一个调用过程、一个函数、一段代码的优化。在这个阶段,各种工具也主要是有效的。除了静态代码优化,还有编译时优化和运行时优化。后两者要求很高,程序员的可控性较弱。

在代码层面,造成性能瓶颈的原因通常是函数调用频繁,函数单次消耗非常高,或者两者兼而有之。

下面介绍设计阶段和实施阶段的优化方法。

一般的做法

缓存

没有缓存解决不了的性能问题。如果有,则添加一级缓存

缓存 /kæʃ/ KASH,[1] 是一个或那个数据,因此对于那个数据可以是 ; 缓存中的数据可能是 of an 或 of data 。

缓存的本质是加快访问速度,访问的数据要么是其他数据的副本——让数据更贴近用户;或先前计算的结果 - 避免重复计算。

缓存需要以空间换时间。在缓存空间有限的情况下,需要出色的替换转换来保证缓存的高命中率。

数据缓存

这是我们最常见的缓存形式,将数据缓存在离用户更近的地方。比如操作系统中的CPU缓存和磁盘缓存。对于一个web应用来说,前端会有一个浏览器缓存,一个CDN,以及一个反向代理提供的静态内容缓存;后端将有一个本地缓存和一个分布式缓存。

数据缓存通常是设计考虑因素。

对于数据缓存,需要考虑缓存一致性的问题。对于分布式系统中具有强一致性要求的场景,可行的解决方案包括租约和版本号。

计算结果缓存

对于消耗大量金钱的计算,可以将计算结果缓存起来,下次直接使用。

我们知道,递归代码一个有效的优化方法就是缓存中间结果,table,避免重复计算。里面的缓存就是这个想法。

对于可能被重复创建和销毁,并且创建和销毁成本较高的对象,例如进程和线程,也可以进行缓存。对应的缓存形式有单例、资源池(连接池、线程池)。

对于计算结果的缓存,也需要考虑缓存失效。对于pure来说,固定的输入就有固定的输出,缓存不会失效。但是,如果计算受到中间状态和环境变量的影响,缓存的结果可能无效,如本文所述:

#

并发

一个人做不完,就找两个人做。并发不仅增加了系统的吞吐量,还减少了用户的平均等待时间。

这里的并发是指广义的并发,粒度包括多机(集群)、多进程、多线程。

对于无状态的服务(状态是指需要维护的上下文环境,用户请求依赖于这些上下文环境),集群可以用来很好的扩展,增加系统的吞吐量,比如挂载nginx后的web

对于有状态的服务,也有两种形式,每个节点提供相同的数据,比如mysql读写分离;每个节点只提供部分数据,例如在

在分布式存储系统中,() 和 () 都有助于并发。

绝大多数的web,要么使用多进程,要么多线程处理用户请求,以充分利用多核CPU,而在IO阻塞的地方,也适合使用多线程。较新的协程 ( , ) 也是一种并发。

懒惰的

将计算推迟到必要时,可以避免冗余计算甚至根本不计算,请参见:

这个想法太棒了!

批处理,合并

当有 IO(网络 IO、磁盘 IO)时,合并操作和批处理操作往往可以提高吞吐量和性能。

我们最常见的是批量读取:每次读取数据时多读取,以备不时之需。例如,GFS 会从 GFS 中读取更多的 chunk 信息;例如,在分布式系统中,如果一个中心化节点生成复杂的全局 ID,我们的应用程序可以一次请求一批 ID。

尤其是当系统中存在单点时,缓存和批处理从本质上减少了与单点的交互,是降低单点压力的一种经济有效的方式

在前端开发中,经常会出现资源的压缩和合并,这也是思路。

对于网络请求,网络传输时间可能比请求的处理时间长很多,所以需要合并网络请求,比如bulk和redis。在写文件的时候,也可以批量写入,减少IO开销,GFS就是这样做的。

更高效的实施

同一个算法必然有不同的实现,所以会有不同的表现;有的实现可能是time-for-space,有的可能是space-for-time,需要根据自己的实际情况权衡。

程序员喜欢造轮子,拿来练习用是可以理解的,但是在项目中,使用成熟的、经过验证的轮子往往比自造轮子要好。当然,不管你是用别人的轮子还是自己的工具,当出现性能问题的时候,要么优化,要么更换。

例如,我们有一个场景,大量复杂的嵌套对象被序列化和反序列化。一开始使用的是()自带的json模块。即使出现性能问题,也无法优化。在线查看并更换。变成ujson,性能好很多。

上面的例子是无损的,但是一些更高效的实现也可能是有损的。例如,如果发现性能问题,很可能会考虑 C 扩展,但也会带来可维护性和灵活性的损失。有坠机风险。

缩小解空间

缩小解空间意味着计算是在较小范围的数据上执行的,而不是遍历整个数据。最常见的是索引。通过索引,可以快速定位数据。数据库的优化主要是索引的优化。

如果有本地缓存​​,那么使用索引也会大大加快访问速度。但是,索引更适合多读少写。毕竟指数的建设也需要消耗。

此外,在游戏服务器端,使用的分割线和AOI(点阵算法)也是减少解空间的方法。

性能优化和代码质量

很多时候,好的代码也是高效的代码,各种语言都会有类似的书《xx》。例如, , 的代码通常非常高效,例如使用迭代器而不是列表(.7 dict(),而不是 items())。

衡量代码质量的标准是可读性、可维护性和可扩展性,但性能优化可能会违反这些特性。例如,为了屏蔽实现细节和使用,我们可能会增加一个接口层(虚拟层),这样可读性、可维护性和可扩展性会好很多,但会额外增加一层函数调用。如果这个地方被频繁调用,也会是开销;就像上面提到的 C 扩展一样,它也会降低可维护性。性别,

这种损害代码质量的优化应该在最后完成,作为最后的手段,并带有清晰的注释和文档。

为了追求可扩展性,我们经常会引入一些设计模式,比如状态模式、策略模式、模板方法、装饰器模式等,但这些模式不一定对性能友好。因此,出于性能考虑,我们可能会编写一些反模式、自定义和不太优雅的代码。这些代码其实是脆弱的,需求的一点点变化可能会对代码逻辑产生至关重要的影响,所以还是回到我之前说的,不要过早优化,不要过度优化。

总结

用脑图来总结一下

性能优化指南:性能优化的一般性原则与方法

郑重声明:本文版权归原作者所有,转载文章仅出于传播更多信息之目的。如作者信息标注有误,请尽快联系我们修改或删除,谢谢。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了