这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
Go 内存分配
分块
- 先使用mmap向系统分配一大块连续内存空间,
- 将分配到的内存空间切分成固定大小的块(如8K),称作 mspan
- 将不同mspan用于固定长度的分配(如8字节, 16字节等)
- noscan mspan: 分配不包含指针的对象, GC不需要扫描
- sacn mspan: 分配包含指针的对象, GC需要扫描
- 分配对象时, 向上取整找到符合的mspan进行分配
缓存
go的内存分配借鉴了TCMalloc的thread caching思想
- 每个p(处理器)对应了自己的分配区mcache, 用于保存当前用到的mspans(非固定大小)
- 应用整体有若干个mcentral缓存, 每个mcentral存储固定大小的mspans, 对应虚拟内存上的堆区域(Heap Arena), 通常是以内存页为分配单元.
- 分配内存时, 首先请求mcache, 无法满足则向mcentral请求mspans, 如果还不能满足则mcentral扩容, 向系统请求新的内存页.
- 分配内存不释放, mcache中的mspans在清空后返回到mcentral中, 不直接返回给操作系统.
优化- Balanced GC
观察
- 对象分配频率非常频繁: 每秒分配GB级别的内存
- pprof 显示对象分配的函数是调用最频繁的函数之一
- 小对象占比较高
- Go 内存分配时延较高
- 分配路径长: g -> m -> p -> mcache -> mspan -> memory block -> return pointer
为g(goroutine)加入分配缓冲区, 缩短分配路径
- g绑定一块内存(1KB), 称作goroutine allocation buffer(GAB)
- GAB 用于noscan 类型的小对象分配: < 128 B
- Bump pointer(指针碰撞, 类似brk), 使用三个指针维护 本地操作, 无需互斥同步, 分配简单快速
if top + size <= end {
addr := top
top += size
return addr
}
- GAB内部的内存碎片使用 copying GC (这是否意味着每个g要对应多个GAB?)
编译器与静态分析
编译器结构
目标:
- 识别符合语法和非法的程序
- 生成正确且高效的代码 编译器前端(分析部分)
- 词法分析, 生成词素 lexeme
- 语法分析, 生成语法书 AST
- 语义分析, 收集类型信息, 进行语义检查
- 中间代码生成, 生成 intermediate representation (IR) 编译器后端
- 代码优化, 机器无关优化, 生成优化后的 IR
- 代码生成, 生成目标代码
静态分析: 不执行程序代码, 推导程序的行为, 分析程序的性质.
- 控制流(Control flow), 程序执行的流程
- 数据流(Data flow), 数据在控制流上的传递
- 过程内分析(intra-procedural analysis), 仅在函数内部进行分析
- 过程间分析(inter-procedural analysis), 考虑函数调用时参数传递和返回值的数据流和控制流, (需要结合上下文, 同时分析控制流和数据流, 较为复杂)
go 编译器优化
编译器优化的优点
- 用户无感知, 重新编译即可获得性能受益
- 通用性优化 现状
- 采用的优化少
- 目标为合理的编译时间, 没有使用较复杂的代码分析和优化 思路: 面向后端长时间执行的任务, 以更高的编译时间换取更高效的机器码 常见优化策略:
- 函数内联
- 逃逸分析
- 默认栈大小调整
- 边界检查消除
- 循环展开
函数内联
- 将被调用函数的函数体(callee)的副本替换到调用位置上(caller), 并以本地变量引用替换参数
- 优点:
- 消除了函数调用开销, 例如传递参数, 保存寄存器等
- 将过程间分析转换为过程内分析, 有利于其他优化, 如逃逸分析
- 缺点:
- 函数体变大, instruction cache 不友好
- 编译生成的 Go 镜像变大
- 函数内联在大多情况下为正向优化
- 调优方向: 内联的策略
- 调用和被调用函数的规模
逃逸分析: 分析代码中指针的动态作用域, 指针在何处可以被访问
- 从对象分配处出发, 沿着控制流, 观察对象的数据流
- 若发现指针p在当前作用域s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的goroutine
- 传递给已逃逸的指针指向的对象
- 则指针p指向的对象逃逸出s, 反之则没有逃逸出s
优化后, 函数内联拓展了函数边界, 更多对象不逃逸, 未逃逸的对象可以在栈上分配
- 对象在栈上分配和回收很快: 移动sp
- 减少在堆上的分配, 降低了GC负担