这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
内存优化是Go语言开发不可或缺的环节,而Go语言内存管理就是优化环节的重要组成部分。在这一章中会对Go语言SDK及其编译器内存管理优化进行详细总结。
优化
业务层优化:针对特定场景,具体问题。
语言运行优化:解决更通用的性能问题。
性能优化注意事项:稳定,测试用例,优化内容文档,通过选项控制是否优化,必要的日志输出。
内存管理优化
- 自动内存管理
相关概念:
Grabage collction: 垃圾回收
Mutator: 业务线程,分配新对象,修改对象指向关系
Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
Serial GC: 只有一个 collector,此时业务线程暂停
Parallel GC: 支持多个 collectors 同时回收的 GC 算法,此时业务线程暂停
Concurrent GC: mutator(s) 和 collector(s) 可以同时执行
GC评价指标:安全,吞吐率,暂停时间,内存开销。
-
追踪垃圾回收
标记根对象,包括全局变量global、堆heap、栈stack。
找到可达对象。
清理不可达对象,包括三种方式:
1、将存活对象复制到另外的内存空间(Copying GC)。
2、将死亡对象的内存标记为"可分配”(Mark-sweep GC)。
3、移动并整理存活对象(Mark-compact GC)。 -
分代GC
根据生命周期选择不同的回收机制。
基于分代假说:很多对象分配出来之后很快就不用了。
不同年龄(经过GC的次数)对象分配在不同heap区域。
对于年轻代,用Copying GC处理。对于老年代,用Mark-compact GC处理。 -
引用计数
每个对象都有一个与之关联的引用数目,当且仅当引用数大于0时对象存活。
优点:程序进行过程中维护对象的计数,甚至不需要了解runtime的实现细节。
缺点:维护开销比较大,无法回收环形数据结构,需要额外内存,回收内存时需要暂停。
Go内存分配
-
为对象在heap上分配内存:
mmap()向OS申请大块内存,分割成次大块内存mspan,继续分割成特定大小的小块,分配不包含指针的对象称为noscan mspan(GC不扫描),分配包含指针的对象scan mspan(GC需要扫描)。
缓存mcache管理一组mspan,当管理的内存分配完了,就向mcentral申请还有没分配块的mspan,对应的,如果mspan中还有没分配块,会缓存在mcentral中,而不是立刻释放归还给OS。 -
Go内存分配流程比较长,需要进行优化。
-
优化方案Balanced GC
每个g分配1KB内存,成为GAB,用于noscan类型对象分配,用三个指针进行维护,需要内存分配时,直接移动指针分配出一块内存。
缺点:导致内存延迟释放。
解决方案:当GAB大小超过一定阈值时,把存活的对象移动到一个新创建的GAB中,类似copying GC。
编译器优化
- 编译器结构
-
静态分析:不执行代码,推到程序行为
控制流(Control flow): 程序执行的流程。
数据流(Data flow): 数据在控制流上的传递。 -
过程内分析和过程间分析
Go编译器优化
-
函数内联(InLinning)
将被调用函数的函数体(callee) 的副本替换到调用位置(caller) 上,同时重写代码以反映参数的绑定。
优点:消除函数调用开销,将过程间分析转化为过程内分析。
缺点:函数体变大,编译生成的Go镜像变大。 -
Beast Mode
调整函数内联的策略,使更多函数被内联。
降低函数调用开销,增加其他优化机会。 -
逃逸分析
分析代码中指针的动态作用域:指针在何处可以被访问。
大致思路:
从对象分配处出发,沿着控制流,观察对象的数据流
若发现指针 p 在当前作用域 s:
作为参数传递给其他函数
传递给全局变量
传递给其他的 goroutine
传递给已逃逸的指针指向的对象
则指针 p 指向的对象逃逸出 s,反之则没有逃逸出 s