这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记
GO-学习笔记
优化
- 性能优化,提升软件系统处理能力,减少不必要的消耗,充分发掘计算算力
- 提升用户体验
- 资源高效利用,降低成本,提高效率
- 业务层、sdk、基础库、语言运行时、os系统
- 业务层,针对特定场景,具体问题具体分析
- 语言运行时,针对通用型问题,主要涉及内存管理和分配,资源调度
- 开发自定义SDK,保证接口稳定,优化具体实现
内存管理
- 动态内存,程序在运行时根据需求动态分配内存
- 自动内存管理,程序运行时系统管理动态内存,避免手动内存管理,保证内存使用的正确性和安全性
- double-free,两次释放相同位置的内存
- use-after-free,使用没有开辟内存的位置
- 三个任务
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的空间
相关概念
- Mutator threads,业务线程,分配新对象,修改对象指向关系
- Collector,GC线程,找到存活对象,回收死亡对象的内存空间
- Serial GC,串行GC,只有一个GC线程,存在STW
- Parallel GC,并行GC,多个GC线程,存在STW
- Concurrent GC,与用户线程并发执行,并发回收,必须感知到对象指向关系的改变
- 三色标记法,存在浮动垃圾和漏标的问题,通过写屏障解决
评价GC
- 安全性,不能回收存活对象
- 吞吐量,1 -(GC时间 / 程序执行总时间),越高越好
- 暂停时间,STW
- 内存开销,GC元数据的开销
Tracing garbage collection
- 追踪垃圾回收,可达性分析,
JVM
- 标记根对象,静态变量、全局变量、常量、线程栈
- 找到可达对象,求指针指向关系的传递闭包
- 清理所有不可达对象
清理策略
- 根据对象的生命周期,使用不同的标记和清理策略
- Copying GC,复制算法,把存活对象,复制到另外的内存空间
- Mark-sweep GC,标记清除算法,把死亡对象的内存空间标记为可分配
- Mark-comparct GC,标记整理/压缩算法,移动并整理存活对象
分代GC
- 大部分对象分配出来之后,很快就不使用了
- 每个对象都有年龄,经过GC的次数
- 分为年轻代和老年代,进行不同的管理策略
- 年轻代
- 存活数量少,采用复制算法
- GC吞吐率高
- 老年代
- 趋于长时间存活,采用标记清除或者标记整理
Reference counting
- 引用计数,每一个对象都有与之关联的引用数目,大于0时活着,
OC/Swift - 优点
- 内存管理操作,平摊到程序执行过程中
- 不需要了解
runtime的实现细节,C++智能指针为引用计数的实现
- 缺点
- 维护开销较大,通过原子操作,保证对引用计数操作的原子性和可见性
- 存在循环引用,弱引用来解决
- 增加内存开销,需要额外的空间存储引用数目
- 回收大范围引用图的内存时,存在STW
Go的内存管理及优化
分块
- 提前将内存分块,根据对象大小,选择最合适的块返回
- 调用系统调用
mmap(),向os,申请一块内存mcache,4MB- 将内存划分为几大块,mspan,8KB,包含一块
mark bits,保存该块中的某块内存是否被标记为空闲,标记清除算法- 将大块
mspan划分为特定大小的小块,用于对象分配
noscan mspan,分配不包含指针的对象,GC不需要扫描scan mspan,分配包含指针的对象,GC需要扫描
缓存
- 借鉴了
TCmalloc,thread caching,对内存做了很多级不同的缓存
TC is short for thread caching
- 为
g goroutine 某个协程,绑定一个p,用于分配对象
- 每个
p包含一个mcache用于快速分配- 每个
mcache管理一组mspan,当mcache中的mspan分配完毕,向mcentral申请未分配块的mspan- 当
mspan中没有分配的对象,会被缓存在mcentral,而不是立即释放归还os
优化内存管理
- 对象分配是非常高频的操作
- 小对象占比比较高
- Go分配对象耗时较长
- 分配路径
g -> m -> p -> mcache -> mspan -> memory block -> return pointermallocgc对象分配函数,调用很频繁
balanced gc
- 类似于
Thread Local Allocation Buffer - 直接改
Go的SDK - 每个
用户 g绑定一大块内存,GAB goroutine allocation buffer
- 用于
noscan类型的小内存分配,小于128B- 额外空间记录对象的起始地址,标记对象头
- 如果用于
scan类型,不好进行GC,其中一个对象活着,可能引用了很多对象,导致这块大内存无法被回收
- 用三个指针维护
GAB,base/end/top - 指针碰撞
bump-pointer风格的对象分配,不会与其他协程上的分配产生互斥,分配简单高效 GAB对Go内存管理来说是一个大对象,本质将多个小对象的分配,合并成一次大对象的分配
会导致内存被延迟释放,
GAB中只存在一个小对象,会导致这一大块内存无法被及时回收
- 移动
GAB中的存活对象
- 当
GAB总大小超过一定阈值,小于一定阈值,将GAB存活的对象复制到另外分配的GAB中,让原先的GAB能够被回收- 在
GoGC最后一个阶段,修改原来指向该存活对象的指针,需要STW,再为该g生成一个GAB- 释放原先的
GAB,避免内存泄漏- 用
复制算法管理小对象,复制到survivor GAB暂存,不属于任何一个g
编译器和静态分析
编译器结构
- 识别符合语法和非法的程序,生成正确且高效的代码
- 分析部分
- 词法分析,生成词素
- 语法分析,生成语法树
- 语义分析,收集类型信息,进行语义检查
- 中间代码生成,生成
IR,intermediate representation 中间表示
- 综合部分
- 代码优化,机器无关的优化,生成优化后的中间表示
- 代码生成,生成目标代码
静态分析
- 生成执行更快,效率更高的机器码
- 不执行程序代码,推到程序行为的结果,分析程序的性质,程序执行是否正确
- Control flow,控制流,程序执行的流程,用
Control-flow graph编排 - Data flow,数据流,数据在控制流上的传递,数据执行过程中某个阶段的值
过程内和过程间分析
- Intra-procedural analysis,过程内分析,函数内部分析
- Inter-procedural analysis,过程间分析,函数调用时参数传递和返回值的数据流和控制流
- 数据流和控制流同时分析,才能推断执行顺序
Go编译器优化
- 用户无感知,重新编译即可获得性能收益,通用性优化
- 优化思路
- 面向后端长期执行的任务
- 用编译时间换取更高效的机器码
- Beast mode,函数内联,逃逸分析,默认栈大小调整,边界检查消除,循环展开
函数内联
- Go内联策略很保守
- 将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
- 优点
- 消除函数调用的开销,例如传递参数、保存寄存器
- 将过程间分析转换为过程内分析,帮助其他优化(逃逸分析)
- 缺点
- 函数体变大,
instruction cache,icache,指令高速缓存不友好- 编译生成的Go镜像变大
- 大多数是正向优化,根据策略决定是否内联
- 根据调用和被调用函数的规模
- Beast Mode
- 调整函数内联策略,降低函数调用开销,增加其他优化机会
- 开销,Go增加10%,编译时间增加
逃逸分析
- 分析代码中指针的动态作用域,指针在何处可以被访问
- 从对象分配出发,沿着控制流,观察对象的数据流
- 逃逸成功,作为参数传递给其他函数,传递给全局变量,传递给其他
goroutine,传递给已逃逸的指针指向的对象
- Beast Mode
- 函数内联拓展了函数边界,更多对象不逃逸
- 对象在栈上进行分配,回收很快,只需移动
栈寄存器 sp StackPointer;减少在堆上的分配,降低GC负担