这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
性能优化
是什么
性能优化是指提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力
为什么
我们之所以要使用性能优化,可以从两方面来说
- 用户体验:带来用户体验的提升,例如刷抖音更顺畅,双十一购物不堵车
- 资源利用:降本增效,成本降低的同时提高了资源的利用率,海量的服务与硬件设施会把一个小优化放大无数倍
优化层面
优化层面可以分为三个部分:业务层优化、语言运行时优化与数据驱动
业务层优化
- 针对特定的场景,具体问题具体分析
- 在这个层面进行优化相对容易获得很大的性能收益
语言运行时优化
- 这部分解决的是更通用的性能问题,不再针对单一业务进行调优
- 会考虑到更多的场景
- Tradeoffs(权衡),我们优化是具有普适性的,因此我们需要考虑哪些可以优化,哪些优化带来的副作用大于正反馈。
数据驱动
- 我们依靠数据进行分析而非猜测
- 首先优化性能瓶颈
- 可以使用自动化性能分析工具==pprof==
软件质量
这里主要是一些概念的解释或理解,便于我们充分理解什么是软件质量
- 测试用例:测试用例需要覆盖尽可能多的场景,方便回归
- 文档:我们做了什么,我们没做什么,我们做的这些可以实现哪些效果(类似中学时代的某些语文作文)
- 隔离:我们可以通过铉锡那个控制是否开启优化
- 可观测:我们必须对软件作出必要的日志输出,便于我们后续观测、维护
自动内存管理(GC)
自动内存管理保证内存使用的正确性与安全性,例如double-free(多次释放同一块内存)与use-after-free(释放后调用内存)的问题我们可以通过自动内存管理来解决
三个任务
自动内存管理具有三个任务,分别是
- 为新对象分配内存空间
- 找到存活对象
- 回收死亡对象的内存空间
相关概念
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector:GC线程,找到存活的对象,回收死亡对象的内存空间
- Serial GC:只有一个collector的GC算法,它会暂停
- Parallel GC:支持多个collector同时回收的GC算法,同样会暂停,但是会用4个线程来回收
- Concurrent GC:mutator(s)和collector(s)可以同时执行的算法,它不需要去暂停程序,可以一边垃圾回收一遍执行Mutator(s)
我们可以用一张图来表示后三个概念
评价GC算法
我们可以使用如下几个方面来评价一个GC算法
- 安全性(Safety):不能回收存活的对象,则是基本要求
- 吞吐率(Throughput):
- 暂停时间(Pause time):stop the world(STW)
- 内存开销(Space overhead):GC元数据开销
追踪垃圾回收
我们需要对垃圾进行追踪,以便于我们销毁,在这里简要介绍一下追踪垃圾回收的过程
- 对象被回收的条件:指针指向关系不可达的对象
- 标记根对象:我们标记静态变量、全局变量、常量、线程栈等
- 标记:找到可达对象,也就是求指针指向关系的传递闭包,从根对象出发,找到所有可达对象
- 清理:清除所有不可达对象
- 将存活对象复制到另外的内存空间(Copying CG)
- 将死亡对象的内存标记为可分配(Mark-sweep CG)
- 移动并整理存活对象(Mark-compact CG)
- 我们根据对象的生命周期,使用不同的标记和清理策略
下文的分代GC体现了最后一个点
分代GC(Generational GC)
这是一种比较常见的自动内存管理方式
分代假说
先讲一下分代假说(Generational hpyothesis):most objects die young
Intuition:我们很多对象就是new了一下,new完进行了一点点的操作就被释放了
对象的年龄:我们定义为经历过GC的次数
目的:针对年轻和年老的对象,制定不同的GC策略,降低整体内存管理的开销
年轻代(Young Generation)
年轻代也就是经历过GC次数较少的对象,我们对这些对象执行
- 常规的对象分配
- 因为存活的对象很少,可以采用 copying collection(复制,然后清理)
- GC吞吐率很高
老年代(Old Generation)
- 对象一直趋于存活,如果采用跟年轻代一样的复制清理开销太大
- 因此我们使用 mark-sweep collection
小结
分代GC其实就是把各个对象分成了两类
一类是销毁次数很少(通常销毁后很长一段时间不会再新建),我们采用copying collection策略
而另一类则是销毁次数很多,但是它存活率高(因为一直在销毁、新建),如果一直复制处理的话开销太大,我们就直接对系统说这块内存一直有用,但是我们在内部对其进行是否可分配的管理
引用计数
这也是一种比较常见的内存管理方式
概念
首先我们需要知道一些概念
- 每个对象都有一个与之关联的引用数目
- 只有当且仅当引用数 > 0,我们才认定对象存活
优点
- 内存管理的操作被平摊到程序执行的过程中
- 内存管理不需要了解runtime的实现细节,我们只需要维护引用计数,一些库可以帮助我们实现自动的引用计数,例如C++的智能指针(smart pointer)
缺点
- 维护的开销会比较大:我们通过原子操作保证对引用计数操作的原子性和可见性
- 我们无法回收环形数据结构: 比如三个对象互相指着,但是没有任何东西指着这三个对象,从上帝视角这个东西应当被清理,但是对于引用计数来说它们会被认为不可清理,在swift中,他们利用了weak reference来解决。
- 内存开销增加:每个对象都引入的额外内存空间存储引用数目
- 回收内存时依然可能引发暂停
参考资料
原子操作是如何实现的?:zhuanlan.zhihu.com/p/33445834