这是我参与「第五届青训营」伴学笔记创作活动的第4天。
今天的内容主要关于go语言发行版的优化,主要包括内存管理优化(垃圾回收)和编译器优化。
前置知识——相关术语:bytedance.feishu.cn/docx/doxcn2…
1 概述
性能优化可以提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力。进而提高用户体验,提高资源利用率。
性能优化的层面可以划分为5个层面:业务代码、SDK、基础库、语言运行时和OS。其中业务代码可以更有针对性地提高性能,语言运行时的提升则更为泛用。对于性能的优化一定要注意基于数据用数据驱动优化。
2 自动内存管理
2.1 使用背景
动态内存管理的底层:c 语言的 malloc。
自动内存管理即在程序运行时由系统管理内存。这样可以让程序员专注于核心代码内容,保证内存使用的正确性和安全性。尤其针对 double-free problem(释放了2次内存), use-after-free problem 可以避免很多问题。
因此,自动内存管理主要完成3个任务:为新对象分配空间、找到存活对象、回收死亡对象的内存空间。
2.1.1 GC 背景知识
线程分类:
- Mutator: 业务线程,分配新对象,修改对象指向关系
- Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
GC模式分类:
- Serial GC: 只有一个 collector (暂停所有 mutator 线程,执行GC,恢复线程)
- Parallel GC: 并行 GC,支持多个 collectors 同时回收的 GC 算法 (暂停,多个 GC 同时工作,恢复线程)
- Concurrent GC: 并发 GC,支持 mutator(s) 和 collector(s) 同时执行的 GC 算法 (无需显式暂停)
Concurrent GC 难点:必须要感知到对象关系的改变.
2.1.2 GC 的评价标准
- 安全性(Safety):不能回收存活的对象
- 吞吐率(Throughput):花在GC上的时间;计算公式 = 1 - GC 时间 / 程序执行总时间
- 暂停时间(Pause Time):stop the world (STW),影响到业务是否会感知到暂停
- 内存开销(Space Overhead):GC 元数据开销
扩展阅读书目:《The Garbage Collection Handbook》
接下来是2种常用的垃圾回收技术。
2.2 Tracing Garbage Collection: 追踪垃圾回收
回收条件:指针指向不可达
操作逻辑:
- 标记根对象:静态变量、全局变量、常量、线程栈
- 找到可达对象:从根对象出发查找(求指针指向关系的传递闭包)
- 清除不可达对象:Copying GC(将存活对象复制到另一块内存空间)、Mark-sweep GC(将死亡对象所在内存块标记为可分配,使用 free list 管理)、Mark-compact GC(将存活对象复制到同一块内存区域的开头)
案例:Generational GC 分代 GC
- 基于分代假说:most objects die young
- 定义“年龄”概念:object 经历过的 GC 次数
- 基于年龄选择不同 GC 策略(本质上是基于不同的生命周期):年轻代使用 Copying GC,老年代使用Mark-sweep GC
2.3 Reference Counting 引用计数
每个对象都有一个引用数目,引用数大于0时对象存活。(有点类似于计算机网络中的 Time to Live -- TTL)
优点:内存管理时间平摊到程序运行过程中;不需要了解runtime细节(类似 C++ 中 smart pointer) 缺点:维护引用计数的开销大;无法回收环形数据结构(swift 中用 weak reference 解决);每个对象都要计数;回收大的数据结构仍可能引入暂停
3 Go 内存管理和优化
3.1 Go 内存分配
总体思想:分块、缓存
用 mmap 申请内存后,go会提前将内存分成大块 mspan,用于对象分配。其中 noscan mspan 用于分配不包含指针的对象,即 GC 不需要扫描;scan mspan 用于分配包含指针的对象,即 GC 需要扫描。
缓存方面借鉴了 TCMalloc (TC: thread caching) ,主要是分层使用内存,适时把内存还给OS。
3.2 Go 内存分配优化
背景:分配内存是高频操作、分配内存占用CPU、小对象占用消耗大
字节解决方案:Balanced GC
4 编译器和静态分析
4.1 编译器基本知识
编译器结构
4.2 静态分析
定义:不执行代码,推导程序的行为,分析程序的性质。
主要分为2种分析方法:
- 控制流分析:程序执行流程(用控制流图 control-flow graph)
- 数据流分析:数据在控制流上的传递(代入数据计算)
4.3 过程内和过程间分析
过程内分析(intra-procedural analysis):针对函数内的控制流和数据流分析
过程间分析(inter-procedural analysis):还要考虑跨函数的控制流数据流分析,如传参
5 Go 编译器优化
目的:用户无感知、优化泛用
字节解决方案:Beast mode
5.1 函数内联 inlining
函数内联:将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码反映参数绑定。
优点:降低开销、把过程间分析转换为过程内分析
缺点:函数体变大、编译器生成的go的镜像变大
需要根据实际情况决定是否要进行内联。
5.2 逃逸分析
逃逸分析:分析指针的动态作用域(在哪里可以被访问)
逃逸的定义:指针是否可以在其作用域之外被访问(如作为传参、被赋值给全局变量、传递给其它goroutine、传递给已逃逸的指针指向的对象)
6 总结与思考
内存管理和优化其实是一个在工业界很重要,但在一个人的实践过程中很难用上到的东西。对于个人项目或小团队而言,优化的性能提升并不足以cover为之付出的成本,但对于大项目而言则必不可缺。
其实在本科的时候学操作系统的内存管理、文件管理,编译系统的工作流程,计算机组成原理之类的课程时,都感受到过很多优化听上去就很好用,但如果真的由自己实现,花费的时间成本过高不说,最终结果也未必有最新技术带来的优化效果好。我想这也是很多人对于优化只有表面了解的原因之一。除此之外,国内学校学习的知识和工业界实际使用的内容之间有很大的壁。无论是代码量、技术是否过时,还是困难的东西让学生感兴趣自行探索,都导致学生在一些看似一步之遥,实际风险很多的步骤上得不到足够的知识。而这些对于工业界来说又太过基础,被认为是经验性的知识。也由此增加了很多试错成本。