这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天
本次内容为课程学习笔记。
Go内存分配
目标:为对象在heap 上分配内存
- 提前将内存分块
调用系统调用mmap()向OS申请一大块内存,例如4MB,先将内存划分为大块,比如8KB,称为mspan,再将mspan的大块内存继续划分成特定大小的小块,用作对象分配,例如第一个mspan每个块划分大小8B,16B等,对mspan做分类,有noscan mspan, 分配不包含指针的对像,所以GC不需要扫描这些对象,scan mspan,会分配包含指针的对象,此时GC需要扫描。
而对象分配即为根据对象的大小选择最合适的块来返回。
- 缓存
Go的内存分配器借鉴了TCMalloc的实现机制,TC指的是 thread caching,通过多级缓存来实现快速内存分配。
每个p包含一个mcache用于快速分配,用于为绑定于p上的g分配对象,分配内存从G出发,然后找到m和p,p上有mcache 存有很多mspans,根据对象大小找到最合适的mspan,如果mcache是满的,从下一级缓存,也即向mcentral申请带有未分配块的mspan,空余的mspan会被缓存在mcentral中,而不是立刻释放并归还给OS。
Go内存管理优化
对象分配对于线上服务是非常高频的操作,每秒分配GB级别的内存。因此需要内存分配的优化。而线上数据,小对象占比较高,绝大多数对象都小于80B,所以针对小对象做特定优化,而且Go内存分配比较耗时,分配路径长,g->m->p-> mcache -> mspan -> memoryblock -> return pointer。在对象分配上消耗CPU较多,基于pprof可以看出,对象分配的函数是最频繁调用的函数之一。
优化方案Balanced GC
GAB: 每个g都绑定一大块内存(1 KB),称为goroutine allocation buffer(GAB), GAB用于noscan类型的<128B的小对象分配。
GAB的指针碰撞: 使用三个指针维护GAB: base(基地址), end(结束地址), top(当前地址),基于该思路,操作top指针移动部分长度,然后返回内存地址,无需和其他分配请求互斥,分配动作简单高效。
if top + size <= end {
addr := top
top += size
return addr
}
原理: GAB对于Go内存管理来说是一个大对象,本质是将多个小对象的分配合并成一次大对象的分配,所以对于大对象走较长的分配路径,而小对象基于指针碰撞分配路径
问题: 如果一个小对象存活,GAB整体都被标记为存活,这种的对象分配方式会导致内存被延迟释放。
解决方案: 当GAB总大小超过一定阈值的时候,移动GAB中存活的对象,将GAB中存活的对象复制到另外分配的GAB中,原先的GAB可以释放,避免内存泄漏,本质是用copying GC的算法来管理小对象。
编译器和静态分析
编译器结构
编译器是重要的系统软件,识别符合语法和非法的程序,进而生成正确且高效的代码。包括前端和后端。
分析部分(前端 ):执行源代码读入;词法分析来生成词素;语法分析来生成语法树; 语义分析将结果放在语法树上,形成decorated AST;收集类型信息,进行语义检查进而生成中间代码,不论是什么编译器都是生成同样一组intermediate representation(IR)
综合部分(后端 ):代码优化是机器无关的优化,生成优化后的IR, 基于不同平台的代码生成,生成目标代码。
静态分析
方法是不执行程序代码,只推导程序的行为。
-
控制流:程序执行的流程
-
数据流:数据在控制流上的传递。
通过分析控制流和数据流,从而了解程序的性质信息,并根据这些信息来优化代码。
关于过程内和过程间分析
-
过程内分析:仅在函数内部进行分析
-
过程间分析:考虑函数调用时参数传递和返回值的数据流和控制流:
过程间分析需要同时分析数据流和控制流,联合求解比较复杂。而通过函数内联可以简化过程间分析。
Go编译器优化
特点:用户无感知,只要重新编译即可获得性能收益,而且通用性优化对所有方向都有手段
场景:面向后端长期执行任务,所以考虑用编译时间换取更高效的机器码。
开发产品Bease mode可以实现如下编译配置:函数内联;逃逸分析;默认栈大小调整;边界检查消除;循环展开
函数内联
内联函数将被调用函数的函数体副本替换到调用位置上,同时重写代码来反映参数的绑定。
-
优点是消除函数调用开销,例如传递参数,保存寄存器等;将过程间分析转换为过程内分析,帮助其他优化,例如逃逸分析等。
-
缺点:函数体变大了,对instruction cache(icache)不友好,而且编译生成的Go镜像会变大。
函数内联在大多数情况下是正向优化。但不能无休止做内联,需要依据内联策略让编译器调整该函数是否该被内联。
改进:Go函数内联受到的限制较多,因为语言特性,例如interface和defer等限制了函数内联,内联策略比较保守。BeastMode调整了函数内联的策略,使得更多函数被内联,降低了函数调用的开销,增加了其他优化的机会,开销上,Go的镜像增加10%,编译时间增加。
逃逸分析
逃逸分析定义为分析代码中指针的动态作用域。也即指针在何处可以被访问。
从对象分配处出发, 沿着控制流,观察对象的数据流。如果指针p在当前作用域s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的goroutine
- 传递给已逃逸的指针指向的对象
则指针p指向的对象逃逸出s,也就是给了其他域访问p指向对象的机会,反之则没有逃逸出s。
Beast mode的改进是函数内联拓展了函数的边界,导致更多对象不逃逸。对象在栈上分配和回收很快,只需要移动sp;减少在heap上的分配对象,降低GC负担。