这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天
Go 内存管理及优化
Go 内存分配 - 分块
在Golang中,其提前将内存分块,过程如下:
- 调用系统调用
mmap()向OS申请一大块内存,比如4MB - 先将内存划分为多个大块,例如8KB,被称为
mspan - 再将大块继续划分成特定大小的小块,用于对象分配。根据不同粒度,可以划分出不同大小的小块,8B,16B,24B...
- noscan mspan:分配不包含指针的对象,此时GC不用扫描
- scan mspan:分配包含指针的对象,此时GC需要扫描
然后根据对象的大小,选择最合适的块将其返回即可
Go 内存分配 - 缓存
Go语言的内存管理参考了TCMalloc,其中T表示Thread,C表示Caching
- 首先根据Go语言的GMP调度模型(G为goroutine,M为Machine也叫thread内核线程,P为Processor处理器,P调度G到M上执行)
- 每一个P都会拥有一个mcache用于快速分配内存(因此不用加锁),也就是给P上的G分配内存
- 其中mcache管理了一组mspan
- 当mspan内存分配完毕后会向mcentral(M共享,加锁访问)申请带有未分配块的mspan
- 当mspan中的块都未被分配时(可能是对象被回收了),mspan会被缓存到mcentral中,而不是立刻释放并归还给OS。如果有需要分配的话就可以立即把这个mspan分配出去。根据一定策略还回给OS。
Go 内存管理优化
mallocgc 内存分配
-
需要知道的一点是:对象分配是非常高频的操作,每秒分配GB级别的内存是很常见的。
-
并且在对象分配中,小对象的占比相对较高,
-
并且内存分配的流程比较耗时
- 分配路径长:
g -> m -> p -> mcache -> mspan -> memory block ->return pointer - 查看 ,对象分配的函数是最频繁调用的函数之一
- 分配路径长:
我们的优化方案:Balanced GC
-
将每个 g 都绑定一大块内存(1KB),称为goroutine allocation buffer(GAB)
-
GAB用于noscan类型的小对象分配:< 128B
-
使用三个指针维护GAB:base,end,top
-
Bump pointer(指针碰撞)风格对象分配
- 无须和其他分配请求互斥
- 分配动作简单高效
GAB对于Go内存管理来说是一个对象。
其本质就是将多个小对象的分配合并成一次大对象的分配。
不过也有缺点,就是GAB的对象分配方式会导致内存被延迟释放(GAB中的一块小内存被释放了,并不会将GAB释放),解决办法就是移动GAB中的存活对象,当GAB总大小超过一定的阈值时,将GAB中存活的对象复制到另外分配的GAB中,这样原先的GAB就可以释放了,从而避免内存泄漏,从本质上看,也就是使用Copying GC来管理小对象。
编译器和静态分析
编译器的结构
编译器是重要的系统软件,它的作用主要有两个:
- 识别符合语法和非法的程序
- 生成正确且高效的代码
对于整个编译器结构来说,其一般被分为分析部分(前端) 和 综合部分(后端) 。
分析部分(前端)
分析部分主要的工作如下:
- 词法分析,生成词素(lexeme)
- 语法分析,生成语法树
- 语义分析,收集类型信息,进行语义检查
- 中间代码生成,生成intermediate representation(IR)
综合部分(后端)
综合部分的主要工作如下:
- 代码优化,机器无关优化,生成优化之后的 IR
- 代码生成,生成目标代码
而我们要做的就是编译器的后端优化
静态分析
静态分析指的是不执行程序代码,推到程序的行为,从而分析程序的性质。
其中有两个相关的概念:
- 控制流(Control Flow):程序执行的流程
- 数据流(Data Flow):数据在控制流上的传递
通过分析控制流和数据流,我们可以知道更多关于程序的性质,从而根据这些性质来优化代码。
过程哪分析和过程间分析
过程内分析(Intra-procedural analysis) 指的是仅在函数内部进行分析
过程间分析(Inter-procedural analysis) 则要考虑过程调用时参数传递和返回值的数据流和控制流。
在i的具体类型未知时,需要根据数据流分析i的具体类型,而分析具体类型时,又产生了新的控制流因为A和B都实现了接口I里的foo()函数,因此过程间分析需要同时分析控制流和数据流,也就是联合求解,是比较复杂的。
而根据控制流分析,可以知道其调用的是A的foo()方法。