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

228 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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)

我们可以用一张图来表示后三个概念

image-20230121231447534

评价GC算法

我们可以使用如下几个方面来评价一个GC算法

  • 安全性(Safety):不能回收存活的对象,则是基本要求
  • 吞吐率(Throughput): 1GC时间程序执行总时间 1 - \frac{GC时间}{程序执行总时间}
  • 暂停时间(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