[ 后端与 Go | 青训营笔记 ]
这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
课前:相关术语解读
自动内存管理
-
Auto memory management: 自动内存管理
-
Grabage collction: 垃圾回收
-
Mutator: 业务线程
-
Collector: GC 线程
-
Concurrent GC: 并发 GC
-
Parallel GC: 并行 GC
-
Tracing garbage collection: 追踪垃圾回收
- Copying GC: 复制对象 GC
- Mark-sweep GC: 标记-清理 GC
- Mark-compact GC: 标记-压缩 GC
-
Reference counting: 引用计数
-
Generational GC: 分代 GC
- Young generation: 年轻代
- Old generation: 老年代
Go 内存管理及优化
- TCMalloc
mmap()系统调用- scan object 和 noscan object
- mspan, mcache, mentral
- Bump-pointer object allocation: 指针碰撞风格的对象分配
编译器和静态分析
- 词法分析
- 语法分析
- 语义分析
- Intermediate representation (IR) 中间表示
- 代码优化
- 代码生成
- Control flow: 控制流
- Data flow: 数据流
- Intra-procedural analysis 过程内分析
- Inter-procedural analysis: 过程间分析
Go 编译器优化
- Function inlining: 函数内联
- Escape analysis: 逃逸分析
课内:Go 内存管理详解
高性能 Go 语言发行版优化的内存管理优化,自动内存管理与 Go 内存管理知识,可行性的优化建议。
重点
- 内存管理优化;
- 编译器优化;
- 自动内存管理和 Go 内存管理机制;
- 编译器优化的基本问题和思路。
细节
为什么要优化
- 提升软件系统处理能力;
- 提供用户体验;
- 资源高效利用。
性能优化的层面
-
业务代码 -> SDK -> 基础库 -> 语言运行时 -> OS;
-
业务层优化;
- 特定场景,具体问题,具体分析;
- 性能收益大。
-
语言运行时优化;
- 解决更通用的性能问题;
- 考虑更多场景。
-
数据驱动;
- 自动化性能分析工具;
- 依靠数据而非猜测;
- 实现优化最大瓶颈。
自动内存管理
概念
动态内存
- 程序运行时根据需求动态分配的内存:malloc();
自动内存管理:垃圾回收,系统管理动态内存
- 避免手动管理;
- 保证内存使用的正确性和安全性;
三个任务
- 为新对象分配空间;
- 找到存活对象;
- 回收死亡对象的内存空间;
Grabage collction: 垃圾回收
追踪垃圾回收
-
条件:指针指向关系不可达的对象;
-
标记根对象;
- 静态变量,全局变量,常量,线程栈;
-
标记:找到可达对象;
- 从根对象出发,找到所有可达对象;
-
清理:所有不可达对象;
- 存活对象复制到另外的内存空间(Copying GC);
- 死亡对象内存标记为可分配(Mark-sweep GC);
- 移动并整理存活对象(Mark-compact GC);
-
根据对象的生命周期,使用不同的标记和清理策略。
Generational GC: 分代 GC
分代假说
- intuition:很多对象分配后就不再使用;
- 对象的年龄:经历 GC 的次数;
- 目的:对年轻和年老对象,制定不同的 GC 策略,降低整体内存开销;
- 不同年龄对象处于 heap 的不同区域。
Reference counting: 引用计数
- 每个对象都有一个与之关联的引用数目;
- 对象存活条件:当且仅当引用数大于 0;
-
优点
- 内存管理的操作被平摊到程序执行过程中;
- 不需要了解 runtime 的实现细节;
-
缺点
- 维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性;
- 无法回收环形数据结构;
- 内存开销大;
- 内存回收时依然可能引发暂停;
Go 内存管理及优化
Go 内存分配
分块
- 目标:为对象在 heap 上分配内存;
- 提前将内存分块;
缓存
- 每个 p 包含了一个 mcache 用于快速分配,用于为绑定与 p 上的 g 分配对象;
- mcache 管理一组 mspan;
- 当 mcache 中的 mspan 分配完毕,向 mcentral 申请带有未分配块的mspan;
- 当 mspan 中没有分配对象,mspan 会被缓存在 mcentral 中,而不是立即释放并归还给 OS;
Go 内存管理优化
-
对象分配是非常高频的操作:每秒分配 GB 级别内存;
-
小对象占比较高;
-
Go 内存分配比较耗时;
- 分配路径长;
- 对象分配函数是最频繁调用的函数之一;
优化方案:Balanced GC
- GAB 对于 Go 内存管理来说是一个大对象;
- 本质:将多个小对象的分配合并成一次大对象的分配;
- 问题:GAB 的对象分配方式会导致内存被延迟释放;
-
方案:移动 GAB 中存活的对象
- 当 GAB 的总大小超过一定阈值的时候,将其中存活对象复制到另外分配的 GAB 中;
- 原先的 GAB 可以释放,避免内存泄漏;
- 本质:用 copying GC 的算法管理小对象;
编译器和静态分析
基本介绍
编译器的结构
数据流与控制流
静态分析
- 不执行程序代码,推导程序的行为,分析程序的性质;
- 控制流:程序执行的流程;
- 数据流:数据在控制流上的传递;
- 通过分析控制流和数据流,可以知道更多关于程序的性质;
- 根据性质优化代码;
过程内和过程间分析
- 过程内分析:仅在函数内部进行分析;
- 过程间分析:考虑函数调用时参数的传递和返回值的数据流和控制流;
Go 编译器优化
-
为什么做编译器优化
- 用户无感知,重新编译即可获得性能收益;
- 通用性优化;
-
现状
- 采用的优化少;
- 编译时间较短,没有进行较复杂的代码分析和优化;
-
优化思路
- 场景:面向后端长期执行任务;
- Tradeoff:用编译时间换取效率更高的机器码;
-
Beast mode
- 函数内联;
- 逃逸分析;
- 默认栈大小调整;
- 边界检查消除;
- 循环展开;
函数内联
-
内联:将被调用的函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定;
-
优点
- 消除函数调用开销;
- 将过程间转为过程内分析,帮助其他优化;
-
缺点
- 函数体变大;
- 编译生成的 Go 镜像变大;
- 函数内联在大多数清况下是正向优化;
逃逸分析
-
分析代码中指针的动态作用域;指针在何处可以被访问;
-
思路
-
从对象分配处出发,沿着控制流,观察对象的数据流;
-
若分析指针 p 在当前作用域 s;
- 作为参数传递给其他函数;
- 传递给全局变量;
- 传递给其他的 goroutine;
- 传递给已逃逸的指针指向的对象;
-
则指针 p 指向的对象逃逸出 s;
-
-
Beast mode:函数内联拓展了函数边界,更多对象不逃逸;
-
优化:未逃逸对象可以在栈上分配
- 对象在栈上分配和回收很快;
- 减少在 heap 上的分配,降低 GC 的负担;
总结
对相关术语有所了解,对自动内存分配有了具体的了解,明白了内存优化的好处。