这是我参与「第五届青训营 」笔记创作活动的第4天
性能优化
基本问题
性能优化是什么
- 提升软件系统处理能力,减少不必要的消耗
为什么要做
- 提升用户体验
- 降低成本,高效利用资源
两个层面
业务层优化
- 针对特定场景的具体问题
- 这种优化可以获得较大的性能收益
语言运行时优化
- 面对更加通用的性能问题
- 需要考虑更多的场景
- 需要做更多的利弊权衡
可维护性
1. 需要保证接口的稳定才能进行改进
2. 需要使用尽可能多的测试用例覆盖更多的场景
3. 需要清晰的文档进行描述
4. 需要对优化进行隔离
5. 需要有必要的日志输出
内存管理与优化
自动内存管理
-
基本概念
动态内存 程序运行时动态分配的内存 自动内存管理(又称垃圾回收) - 程序语言的运行时系统自动管理动态内存 - 避免手动内存管理,使程序员可以专注于实现业务逻辑 - 保证了内存使用的正确与安全 自动内存管理的三个任务 - 为新对象分配空间 - 找到存活对象 - 回收死亡对象的内存空间 相关术语 - Mutator: 业务线程,用户启动的线程,分配新对象,修改对象指向关系 - Collector: `GC`线程,找到存活对象,回收死亡对象的内存空间 - Serial GC: 只有一个`collector`执行回收的GC操作,此时`mutator`处于暂停状态 - Parallel GC: 并行`GC`,支持多个`collectors`同时执行回收的`GC`操作,此时`mutator`处于暂停状态 - Concurrent GC: 并发`GC`,支持`mutator(s)`和`collector(s)`同时执行的`GC`算法 GC算法的评价指标 - 安全性: 不能够取回收存活的对象,这是最基本的要求 - 吞吐率: 除去GC花费的时间后剩余时间占程序执行总时间的比例 - 暂停时间: 执行垃圾回收时,将业务暂停的时间,牵涉到业务是否对GC有感知 - 内存开销: GC元数据占用的内存开销 -
追踪垃圾回收
对象被回收的条件 指针指向关系不可达的对象 步骤: - 标记根对象 包括静态变量、全局变量、常量、线程栈等 - 找到并标记可达对象 求指针指向关系的传递闭包,即从根对象出发,找到所有可达对象 - 清理所有不可达对象 - 有三种策略,需要根据对象的生命周期使用不同的标记和清理策略 - 第一种: 将存活对象直接复制到另外的内存空间,原内存空间直接清空 - 第二种: 将死亡对象的内存标记为可分配,之后可以直接进行分配 - 第三种: 移动并整理存活对象,将存活对象移动到一个特定位置,如开头。之后分配内存时从特定位置附近开始分配,如紧跟之后进行分配 -
分代GC
基于分代假说而设立 前提 假设很多对象在分配出来后很快就不再使用了 观点 每个对象都有年龄,即经历过GC的次数 目的 对年轻和老年的对象,知道不同的GC策略,降低整体内存管理的开销 不同年龄的对象处于不同的区域 - 年轻代 - 常规的对象分配 - 存活对象少,使用前文提到的第一种清理策略 - GC吞吐率高 - 老年代 - 对象趋向于一直活着,反复复制开销大 - 使用前文提到的第二种清理策略 - 碎片过多时,使用第三种清理策略进行整理 -
引用计数
每个对象都有一个相关联的引用数目 存活条件 引用计数大于0 优点 - 内存管理操作被平坦到程序执行过程中 - 内存管理不需要了解运行时的实现细节 缺点 - 维护引用计数开销大,需要通过原子操作保证原子性,开销较大 - 环形结构无法回收 - 需要引入额外的内存空间,存在一定的内存开销 - 回收较大的数据结构时,难以避免会引发暂停
Go的内存分配
-
Go内存分配 - 分块
目的 在堆上为对象分配内存 做法 提前将内存分块 操作步骤 - 调用系统使用`mmap()`向OS申请一大块内存 - 将内存划分成大块,称为mspan - 再将大块划分为特定大小的小块,用于内存分配 mspan分为两种 - noscan mspan: 分配不包含指针的对象,即GC不需要扫描 - scan mspan: 分配包含指针的对象,即GC需要扫描 对象分配时 根据对象大小,在mspan中,选择最合适的小块返回 -
Go内存分配 - 缓存
目的 在堆上为对象分配内存 做法 维护一个mcache用于快速分配,在mcache中管理一组mspan用于分配,不足时向下一级缓存申请mspan。 操作步骤 - 首先通过协程找到她所属的p,并通过p找到mcache - 当mcache中存在未分配的块,即mspan未分配完毕时,直接返回。 - 当mcache中不存在未分配的块,即mspan分配完毕时,mspan向下级缓存,即mcentral申请带有未分配块的mspan。 - 当mcache的一组mspan中所有对象都已经释放,此时这一组mspan会归还给下一级缓存,即mcentral中。并不会立刻归还给OS,下一级缓存会按照一定的策略将mspan归还给OS。
Go的内存管理优化
-
问题
内存分配非常高频 小对象占比高 Go内存分配比较耗时,分配路径长,操作频繁 -
优化方案
Balanced GC 操作步骤 - 为每一个协程分配一大块内存,称为GAB - GAB用于noscan的小对象的分配 - 使用三个指针维护GAB - 只需要进行指针操作即可完成内存分配 本质 将多个小对象的分配合并成一次大对象的分配,减少内存分配次数 问题 GAB的对象分配方式会导致内存被延迟释放,比如GAB中仅有一个小对象存活时,整个GAB都是存活的,从而导致了内存的延迟释放 解决方案 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中,原先的GAB就可以进行释放,防止内存泄漏。
编译器
编译器与静态分析
-
编译器
识别符合语法的程序,并通过词法、语法、语义分析和代码优化进而生成可执行的二进制文件 -
静态分析
不执行程序代码,通过控制流分析、数据流分析的方法推导程序的行为,进而分析程序的性质,从而优化程序
编译器优化
-
函数内联
将调用函数的函数体副本替换到调用位置上,同时重写代码以反映参数的绑定 优点 消除了函数调用开销 将过程间分析转化为过程内分析,帮助其它优化 缺点 函数体变大 编译生成的Go镜像变大 -
Beast Mode
调整函数内联策略,使更多函数被内联 -
逃逸分析
分析代码中指针的动态作用域,即指针在何处可以被访问 优化 未逃逸对象可以在栈上分配,此时分配和回收都很快,而且可以减少堆上的分配,降低GC负担
个人总结
本次课程中接触到了一些性能优化的思路,主要针对内存的优化和编译器的优化。同时这些优化思路在其它语言的优化中也可以提供借鉴作用。
引用
- 字节内部课: 高质量编程与性能调优实战