这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
本次编程学习的基本环境配置如下
OS: macOS 13.1
IDE: Goland 2022.3
Go Version: 1.18
重点内容
- 关于高性能 Go 语言发行版优化的内存管理优化
- 自动内存管理与 Go 内存管理知识,提供可行性的优化建议
- 编译器相关知识
- 探讨目前Go内存管理过程中问题,提出解决方案,
- 通过对编译器基本算法讲解,引出编译器优化路径。
优化
- 性能优化:提升软件系统处理能力, 减少不必要的消耗,充分发掘计算机算力
- 性能优化的好处:
- 提升用户体验:带来用户体验的提升
- 高效利用资源:降本增效,很小的优化乘以海量机器会是显著的性能提升和成本节约
- 性能优化的层面 (业务代码->SDK和基础库->Runtime->OS)
- 业务层优化: 针对特定场景,容易获得较大性能收益
- 语言运行时优化:解决更通用的性能问题,考虑更多场景,需要做好tradeoff
- 使用数据驱动的方法
- 自动化性能分析工具-pprof
- 依靠数据而非猜测
- 首先优化最大瓶颈
- 性能优化和软件质量
- 在保证接口稳定的前提下改进具体实现
- 测试驱动开发: 覆盖尽可能多场景,方便回归
- 写文档: 什么要这么做, 要达到怎样的效果, 方便用户选择是否开启优化
- 隔离: 通过选项控制是否开启优化, 没开启的时候, 和没优化前是一样的
- 可观测: 给出必要的日志输出, 告诉用户 已经 开启了该优化
自动内存管理
概念
- 动态内存: 程序在运行时更具需求动态分配的内存
- 自动内存管理(垃圾回收): 由程序语言的运行时系统管理动态内存
- 避免手动内存管理
- 保证内存使用的正确性和安全性, 避免 Double-Free 和 User-After-Free problem
- 三个任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
- Concurrently: bi
- Mutator threads: 业务线程, 分配新对象, 修改对象指向关系
- Collector: GC线程, 找到存活对象, 回收死亡对象的内存空间
- Serial GC: 只有一个collector, GC时暂停程序
- Parallel GC: 支持多个collectors同时回收的GC算法, GC时暂停程序
- Concurrent GC: mutator(s)和collector(s)同时执行, Collectors必须感知对象指向关系的改变
评价GC算法
- 安全性/正确性: 不能回收存活的对象
- 吞吐率: 1 - GC时间 / 程序执行总时间 (程序花在GC上的时间)
- 暂停时间: stop the world(STW) 业务是否感知
- 内存开销: (space overhead) GC元数据开销, 越小越好
Tracing GC 追踪垃圾回收
- 对象被回收的条件: 指针指向关系不可达的对象
- 标记根对象
- 静态变量, 全局变量, 常量, 线程栈等 都是必须存活的对象
- 标记: 找到可达对象
- 找指针指向关系的传递闭包: 从根对象出发, 找到所有可达对象
- 清理: 所有不可达对象
- 将存活对象复制到另外的内存空间(Copying GC), 移动
- 将死亡对象的内存标记为"可分配"(Mark-sweep GC), free-list
- 移动并整理存活对象(Mark-compact GC), 原地整理对象(放到开头)
- 根据对象的生命周期, 使用不同的标记和清理的策略
分代GC(Generational GC)
- 分代假说: 大多数对象很快释放
- 每个对象都有年龄, 即经历GC的次数
- 目的: 对年轻和老年的对象, 制定不同的GC策略, 降低整体内存管理的开销
- 不同年龄的对象处于heap的不同区域
- 对于年轻代, 由于存货对象很少, 可以采用 copying collection, GC吞吐率很高
- 对于老年代, 对象趋向于一直或者, 反复复制开销大, 可以用 mark-sweep collection
Reference Counting 引用计数
- 每个对象都有一个与之关联的引用计数(用图表示)
- 对象存活的条件: 当且仅当引用计数大于0
- 优点
- 内存管理的操作被平摊到程序执行过程中
- 内存管理不需要了解runtime的实现细节, C++智能指针(smart pointer)
- 缺点
- 维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
- 无法回收环形数据结构 可以解决:
weak reference - 内存开销:每个对象都引入的额外内存空间存储引用数目
- 回收内存时依然可能引发暂停(引用图太大的时候)
Go内存管理和优化
分块
- 目标: 为对象在heap上分配内存
- 提前对内存分块
- 调用系统调用
mmap()?不是malloc()向OS申请一大块内存,例如4MB - 先将内存划分成大块,例如8KB,称作mspan
- 再将大块继续划分成特定大小的小块,用于对象分配
- noscan mspan:分配不包含指针的对象一GC不需要扫描
- scan mspan:分配包含指针的对象一GC需要扫描
- 调用系统调用
缓存
- TCMalloc: thread caching
- 每个p包含一个mcache用于快速分配, 用于为绑定于p上的g分配对象
- mcache管理一组mspan
- 当mcache中的mspan分配完毕, 向mcentral申请未分配块的mspan
- 当mspan没有分配的对象, mspan会被缓存到mcentral中, 而不是立刻释放并归还给OS
Go内存管理优化
- 对象分配是非常高频的操作
- 小对象占比较高
- Go内存分配比较耗时: g -> m -> p -> mcache -> mspan -> memory block -> return ptr (g是Goroutine?)
- Balanced GC
- 每个 g 都绑定一大块内存(1KB), 称为 goroutine allocation buffer
- GAB用于noscan内存的小对象分配 < 128B
- 使用三个指针维护GAB: base, end, top
- Bump Pointer(指针碰撞)风格对象分配
- 无需和其他分配请求互斥
- 分配对象简单高效(类似于栈内存分配)
- GAB对于Go内存管理来说是一个大对象
- 本质: 将多个小对象的分配合并成一次大对象的分配
- 问题: GAB的对象分配方式会导致内存被延迟释放
- 当GAB总大小超过一定阈值时, 将GAB中存货的对象复制到另外分配的GAB中
- 原先的GAB可以释放, 避免内存释放
- 本质: 用copying GC的算法管理小对象
编译器和静态分析
编译器的结构
- 编译器是非常重要的系统软件
- 识别符合语法和非法的程序
- 生成正确且高效的代码
- 分析部分(前端)
- 词法分析: 生成词素
- 语法分析: 生成语法树
- 语义分析: 收集类型信息, 进行语义检查
- 中间代码生成: 生成中间表示(IR)
- 综合部分(后端)
- 代码优化, 机器无关优化, 生成优化后的IR
- 代码生成, 生成目标代码
静态分析
- 静态分析: 不执程序代码, 退到程序的行为, 分析程序的性质
- 控制流: 程序执行的流程 ==> 控制流图(Control-flow Graph)是以基本块为节点的图
- 数据流: 数据在控制流上的传递
过程内分析和过程间分析
- 过程内分析: 仅在函数内部进行分析
- 过程间分析: 考虑函数调用时参数传递和返回值的数据流和控制流
过程间分析是个问题, 需要同时分析控制流和数据流--->联合求解, 比较复杂
Go编译器优化
- 为什么做编译器优化
- 用户无感知,重新编译即可获得性能收益
- 通用性优化
- 现状
- 采用的优化少
- 编译时间较短,没有进行较复杂的代码分析
- 优化编译优化的思路
- 场景:面向后端长期执行任务
- Tradeoff:用编译时间换取更高效的机器码
- Beast mode
- 函数内联
- 逃逸分析
- 默认栈大小调整
- 边界检查消除
- 循环展开
函数内联(lnlining)
- 内涵: 将被调用函数的函数体(callee)的副本替换到调用位置(caller)上, 同时重写代码以反映参数的绑定
- 优点
- 消除函数调用开销, 例如传递参数, 保存寄存器等
- 将过程间分析转化成过程内分析, 帮助其他优化, 比如逃逸分析
- 缺点
- 函数体变大, icahce不友好
- 编译生成的Go镜像变大, 编译时间增加
- 函数内联在大多数情况下是正向优化
- 内联策略, 如调用和被调函数的规模
逃逸分析
分析代码中指针的动态作用域: 指针在何处可以被访问
思路
- 从对象分配处出发, 沿着控制流, 观察对象的数据流
- 若发现指针p在当前作用s有以下行为, 则指针p指向的对象逃逸出s
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的goroutine
- 传递给已逃逸的指针指向的变量
函数内联扩展了函数边界, 很多对象不逃逸了
- 未逃逸的对象可以在栈上分配和回收
- 减少在heap上的分配, 降低GC负担
总结
本节讲述了Go内存分配和编译器结构和优化相关知识, 分析问题的方法与解决问题的思路, 不仅适用于Go语言, 其他语言的优化也同样适用.
引用
- 字节内部课-性能优化以及自动内存管理:juejin.cn/course/byte…
- 字节内部课-Go 内存管理 & 编译器优化思路: juejin.cn/course/byte…