当前位置: 首页 > 科技观察

浅谈软件性能优化全景

时间:2023-03-21 20:38:51 科技观察

本文转载自微信公众号《码砖杂工》,作者我不想种田。转载本文请联系码砖手公众号。性能优化是指在不影响正确性的情况下使程序运行得更快,这是一个非常广泛的话题。软件产品种类繁多,影响程序执行效率的因素也很多。因此,性能优化,尤其是对于不熟悉的项目,并不是一件容易的事情。性能优化可以分为宏观和微观两个层面。宏观层面包括架构重构,微观层面包括算法优化、编译优化、工具分析、高性能编码等,这些方法很可能独立于具体的业务逻辑,因此具有更广泛的适应性,更容易实现.实施。具体到性能优化的方法论上,首先要建立指标,衡量什么,得到什么。因此,性能优化测试必须以数据为依据,不能凭空猜测。这是优化的基本原则。搭建一个真实的压力测试环境,或者说接近真实的环境,有时会很困难,可能会花费很多时间,但仍然是值得的。有很多工具可以帮助我们定位程序瓶颈,有些工具可以做非常友好的图形化展示。定位问题是解决问题的前提,但定位问题可能不是最难的。分析和优化是最耗时的关键环节,修改后需要回归测试验证是否达到预期效果。什么是高性能程序?架构博大精深,实现尽可能精微。架构优化的关键是找出瓶颈。这类优化的方法有很多,比如通过负载均衡进行分布式改造,用多线程协程进行并行改造,用消息队列异步解耦,用事件通知代替轮询,比如为数据访问增加缓存,比如使用批处理+预取来提高吞吐量,比如IO和逻辑分离,读写分离等。结构调整和优化虽然非常有效,但由于各种实际限制并不总是可行的。不能做的尽量不做,必须做的高效做是性能优化的基本法则。提高处理能力和减少计算量可以看作是性能优化的两个方向。如何让程序运行得更快?这就需要我们充分利用硬件的各种特性,想方设法减少等待和提高并发,提高缓存命中率,使用更高效的结构和算法;而减少计算量可能意味着跳出纯技术的范畴,从产品和业务的角度来看:哪些功能是必须的,哪些功能是可选的和可配置的。有时,我们不得不从细节的角度来改进程序。通常,我们应该使用简单的数据结构和算法,但如果有必要,我们应该积极使用更高效的结构和算法,不仅是逻辑结构,物理结构(实现)也会影响执行效率;分支预测、反馈优化、启发式和基于机器学习的编译优化效果越来越突出;熟练掌握编程语言,深入理解标准库实现,可以帮助我们避免低性能陷阱;代码微调甚至指令级优化的深入细节有时可以达到意想不到的效果。有时候,我们需要做一些交换,比如用空间代替时间,比如为了高性能牺牲一些通用的可读性,只有在非常必要的时候才去做,这就是取舍的艺术。##1.架构优化###负载均衡负载均衡其实就是解决分布问题。对于分布式系统,负载均衡器一般放在逻辑服务器之前。例如,NGINX就是一个经典的解决方案。负载平衡不限于分布式系统。对于多线程架构的服务器,还需要解决负载均衡的问题,平衡各个工作线程的负载。###多线程、协程并行虽然硬件架构的复杂性对程序开发提出了更高的要求,但是充分利用多CPU、多核的特性编写程序可以获得惊人的收益。因此,在相同硬件规格的情况下,基于多线程/协程的并行改造还是值得尝试的。多线程不可避免地面临着资源竞争的问题。我们的设计目标应该是充分利用硬件多执行核的优势,减少等待,让多次执行能够流畅快速的运行。对于多线程模型,如果把每个要做的任务抽象成一个任务,把工作的线程抽象成一个worker,那么典型的设计思路有两种,一种是划分任务类型,让一个class或者一个worker可以做一个特定的task,另一个就是让所有的worker做所有的task。第一种划分可以减少数据竞争,编码实现更简单。它只需要识别有限的竞争就可以使系统运行良好。缺点是任务的工作量可能不同,可能会导致部分工人比较忙。其他都是免费的。第二种划分的优点是可以平衡,缺点是编码复杂度高,数据竞争多。有时,我们会结合以上两种模式,比如让一个单独的线程做IO(收发包)+反序列化(生成协议任务),然后启动一批worker线程处理包,通过一个任务队列连接中间是经典的生产者消费者模型。协程是基于用户态任务切换的成本低于系统线程切换的假设的用户态多执行流。###Notificationinsteadofpolling轮询是不停的询问,就像你隔几分钟去宿舍看看有没有信,而通知就是你告诉宿舍阿姨你有信了,她会调用来通知你,显然轮询很耗CPU,而通知机制效率更高。###添加缓存缓存的理论依据是局部性原理。通常,系统的写请求远小于读请求。对于写少读多的场景,很适合引入缓存集群。在写数据库的时候,同时写一份数据到缓存集群,然后使用缓存集群来承载大部分的读请求,因为缓存集群容易实现高性能,所以,在这种情况下,通过缓存集群,可以用更少的机器资源承载更高的并发。缓存命中率一般可以很高,而且速度很快,处理能力也很强(单机可以轻松做到上万并发),是比较理想的方案。CDN本质上就是缓存,将大量用户访问的静态资源缓存在CDN中是一种常见的做法。###MessageQueue消息队列和消息中间件用于异步写入请求。当我们向MessageQueue写入数据时,就认为写入完成了,MQ慢慢写入DB。具有削峰填谷的作用。消息队列也是一种解耦手段,主要用来解决写的压力。###IO和逻辑分离,读写分离IO和逻辑分离,这个之前有讲过。读写分离是数据库应对压力的常用措施。当然,不限于DB。###批处理和数据预取批处理是一种思想,可以分为很多种应用,比如对多个网络数据包进行批处理,就是指将接收到的数据包收集在一起,然后一起处理。这样,一个函数被调用多次,或者一段代码被重复执行,那么i-cache的局部性就很好。另外,如果一个section中的函数或者要访问的数据被多次访问,d-cache的局部性也可以提高,自然可以提高性能,批处理可以提高吞吐量,但通常会增加延迟。批处理思想的另一个应用是记录到磁盘。例如,一条日志可能会写入几十个字节。我们可以缓存它并保存足够一次写入磁盘。这样会提高性能,但是也会带来数据丢失的风险,但是通常我们可以通过shm来规避这种风险。指令预取由CPU自动完成。数据预取是一项非常有技巧的工作。数据预取的基础是预取的数据将在下一个操作中使用。符合空间局部性原则。数据预取Fetching可以填充管道并减少内存访问等待,但数据预取会破坏代码并且并不总是像预期的那样有效。即使您不添加预取代码,硬件预取器也可以帮助您进行预取。另外gcc还有编译选项,会在编译阶段自动插入预取代码。手动添加预取代码需要小心处理。时机非常重要。,最后必须以测试数据为依据。另外,即使性能再好,代码修改也可能导致效果衰减,而且prefetch语句的执行本身就有开销。仅当预取的好处大于预取的开销时才值得。好累啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!有时间再写下一章吧##2.算法优化###哈希腊(HASH)####哈希与字符串比较####HashMap####哈希与平衡搜索树比较###二进制SearchBasedonOrderedArray###数据结构实现优化###Lazycalculation©-on-write###Precomputation###Incrementalupdate##3.代码优化###内存优化Smallobjectallocator内存分配分离和对象构造###缓存优化i-cache优化,d-cache优化,缓存对齐,结构重排###判断前置###整体操作代替小操作###多路复用###减法####减少冗余####减少拷贝,零拷贝####减少参数数量(寄存器参数,根据ABI约定)####减少函数调用/层级的数量####减少存储引用的数量####减少无效初始化和重复赋值###LoopOptimization###防御性编程就够了###releaseclean###谨慎使用递归##4.编译优化###inline###restrict###LTO###PGO###优化选项##5.其他优化###绑定核心###SIMD###锁和并发####锁粒度####无锁编程####Per-cpu数据结构&线程本地####总结memorybarrier性能优化是一项细致的工作,工程师们努力寻找捷径来一劳永逸地解决性能问题。不幸的是,没有灵丹妙药,但这并不意味着性能优化没有规则。软件工程师在性能优化方面积累了大量经验,包括架构、缓存、预取、工具、编译器和编程语言、代码重构等实践经验。这些方法和讨论具有参考意义。性能优化也是一个系统工程,性能瓶颈后优化是一种先污染后治理的思路。更好的方式是将性能贯穿于软件的整个生命周期,在设计之初就将性能作为需求甚至关键目标,在开发过程中持续监控性能变化,并严格遵循高性能编码标准。维护将性能纳入维护系统。严格来说,性能优化不同于性能设计。性能优化通常是在现有系统和代码的基础上进行改进。是设计者的正向设计能力,而性能优化方法可以指导性能设计,两者相辅相成。