性能优化与自动内存管理 1| 青训营笔记

73 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天

性能优化

什么是性能优化

提升软件系统处理能力,减少不必要的消耗,充分发挥计算机算力。

为什么做性能优化

  • 带来用户体验的提升

  • 高效利用资源,降低成本,提高效率

性能优化层面

首先,软件层级结构按照从顶层到底层的顺序,依次为业务代码,SDK,基础库,语言运行时(GCC和调度器)和OS。

性能优化往往从两个方面入手:

  • 业务层优化:针对特定场景,具体问题,具体分析。例如业务代码有冗余数据产生,可以针对具体问题解决性能瓶颈从而获得较大的收益。

  • 语言运行时优化:GO 语言运行时的优化,对所有Go使用者都受益,其要点是解决更通用性问题,考虑更多场景。

做优化时要注意的原则:数据驱动,基于pprof等自动化性能分析工具,依靠数据而非猜测,首先优化最大瓶颈。

性能优化的可维护性

性能优化过程中,要注意软件质量至关重要,在保证接口稳定的前提下改进具体实现;测试用例要覆盖尽可能多的场景;文档要体现出做了什么,没做什么,能达到怎样的效果;隔离性是通过选项控制是否开启优化,开启前后保证行为一致性和产品稳定性;最后保证优化产品的可观测性,通过必要的日志输出来提醒用户其优化的收益。

自动内存管理

背景和概念

内存管理,其管理对象是动态内存,也即程序在运行时基于需求执行动态分配的内存,而自动内存管理(垃圾回收),是由程序语言的运行时系统管理动态内存,既能避免程序员手动内存管理,专注于实现业务逻辑,又能保证内存使用的正确性和安全性。

在C++等语言中可能会出现多次释放内存,以及在使用已经释放的内存等问题,而使用自动内存管理可以有效避免该问题。自动内存管理其三个核心任务是:

  • 为对象分配新的空间

  • 找到存活的对象

  • 回收死亡对象的内存空间。

常用概念如下:

  • mutator threads: 业务线程,分配新对象,修改对象的指向关系
  • collector threads: GC线程,执行GC代码,找到存货对象,回收死亡对象的内存空间
  • serial GC: 一种GC算法,只有一个collector,会暂停mutator,然后检查和回收对象
  • parallel GC: 支持多个collectors同时回收的GC算法,还是会暂停mutator
  • concurrently GC: mutator和collector可以同时执行

注意,concurrently GC的挑战是collectors必须要感知对象指向关系的改变。比如在GC过程中会出现已经标记存在的指向未被标记的对象,此时未被标记的应该被标记为存活,如果漏掉可能会报错。

评价GC算法如下:

  • 安全性:不能回收存活对象
  • 吞吐率:1-GC/程序执行总时间
  • 暂停时间:stop the world 的时间越短越好,避免业务有感知
  • 内存开销:GC元数据开销

追踪垃圾回收

对象被回收的条件:指针指向关系不可达对象

标记根对象:首先标记静态变量,全局变量,常量和线程栈

标记可达对象:指针指向关系的传递闭包,从根对象触发,找到所有可达对象

清理所有不可达对象,方法如下:

  • copy GC:将存活复制到其他的内存空间,将死亡对象的内存标记为可分配

  • mark-sweep GC:使用freeList管理内存,下次分配内存直接用链表的内存块

  • compact GC:原地复制的方法,将存活拷贝到内存开始的地方,根据一定策略进行压缩

自动内存管理的思想:根据对象生命周期来使用不同的标记和清理策略。

分代GC(Generational GC)

分代假说是大部分对象在函数中创建完成后很快就回收了,寿命很短。

设定对象的年龄为经历GC次数,对年轻对象和老对象指定不同的GC策略,来降低整体内存管理开销:

  • 年轻代:常规的对象分配,由于存活对象很少,可以采用copying collection GC吞吐率很高

  • 老年代:趋向于一直存活,反复复制的开销大,可以采用mark-sweep collection 来做。

引用计数(reference counting )

每个对象都有一个与之关联的引用计数:当且仅当引用计数大于0对象可存活。

优点在于,内存管理的操作被平摊到程序执行过程,内存管理不需要了解runtime的实现细节,类似C++的智能指针。

缺点在于,维护引用计数的开销较大,通过原子操作保证对引用的原子性和可见性;而且无法回收环形数据结构,考虑使用weak reference解决; 有一定内存开销,比如每个对象都引入的额外内存空间存储引用数目;回收内存时依然可能引发暂停。