这是我参与「第三届青训营 -后端场」笔记创作活动的的第4篇笔记。
优化是优化什么
-
优化
- 内存管理优化
- 编译器优化
-
背景
- 自动内存管理和Go
追求极致性能
- 性能优化是什么?
提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力
-
为什么要做性能优化
- 用户体验
- 资源高效利用:降低成本,提高效率
性能优化层面
有很多,本文主要聚焦业务层优化和语言运行时优化
-
业务层优化
- 针对特定场景,具体问题,具体分析
- 容易获得较大性能收益
-
语言运行时优化
- 解决更通用的性能问题:内存分配,编译器,代码质量
- 考虑更多场景
- Tradeoffs(权衡取舍)
-
数据驱动
- pprof
- 依靠数据而非猜测
- 首先优化最大瓶颈
性能优化与软件质量
- 测试用例:覆盖尽可能多的场景,方便回归
- 隔离:通过选项控制是否开启优化
- 可观测:必要的日志输出
- 软件质量至关重要
- 在保证接口稳定的前提下改进具体实现
- 文档:做了什么,没做什么,效果如何
总结
-
性能优化的基本问题
- 内存管理
- 编译器优化
-
两个层面
- 业务层优化
- 语言运行时优化
-
可维护性
一、自动内存管理
-
动态内存
-
程序在运行是根据需求动态分配的内存:malloc()
-
自动内存管理
- 避免手动内存管理,专注业务实现
- 保证内存使用的正确性和安全性
-
三个任务
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
-
-
相关概念
collectors必须感知对象指向关系的改变
因为这时用户线程也在执行,很可能又指向了新的对象,这时这个新对象也必须被标记为存活。
追踪垃圾回收
- 对象被回收的条件:指针指向关系不可达的对象
- 标记根对象
- 静态变量,全局变量,常量,线程栈等(感觉和java GC root选择挺像)
- 标记:找到可达对象:求指针指向关系的传递闭包,从根对象出发
- 清理:清理所有不可达对象
- 将存活对象复制到另外内存空间 (Copying GC) 复制到另外内存空间
- 死亡对象标记为可分配(Mark-sweep GC) free list管理
- 移动并整理存活对象(Mark-compact GC) 原地整理
- 根据对象生命周期,使用不同标记和清理策略
分代GC
- 年轻代:存活对象很少,吞吐率高,采用copying collection
- 老年代:反复复制开销较大,mark-sweep collection
引用计数
-
优点:
- 内存管理操作被平摊到程序执行过程中,顺带着就完成了
- 内存管理不需要了解runtime的实现细节
-
缺点
- 开销大:通过原子操作保证原子性和可见性
- 无法回收环形数据结构 weak reference(swift语言)
- 内存开销:每个对象引入额外空间存储引用数目
- 回收内存依然可能引发暂停(比如root为null,被回收的对象很多)
-
总结:自动内存管理背景意义,概念和评价方法,追踪垃圾回收,引用计数,分代GC
二、Go内存管理及优化
Go内存分配----分块
- 目标:为对象在heap上分配内存
- 提前将内存分块
- 对象分配:根据对象大小,选择最合适的块返回
Go内存分配----缓存 g表示goroutine,m表示machine,是os线程模型,代表真正执行计算的资源,p代表processor,相对于g是cpu,g只有绑定到p上才可以被调度执行.
- TCMalloc
- 每个p包含一个mcache用于快速分配,用于为绑定在p上的g分配对象
- mcache管理一组mspan
- mcache中mspan分配完毕,向mcentral中申请带有未分配块的mspan,再插到mcache里,如果还不够,再去heap上申请内存用来分配
- mspan没有分配的对象,mspan会被缓存再mcentral中,而不是立刻释放并归还给OS
Go内存管理优化
-
对象分配非常高频:每秒分配GB级别
-
小对象占比高
-
Go内存分配比较耗时
- 分配路径长
- pprof:最频繁调用的函数
-
字节优化方案:Balanced GC
-
每个g绑定一大块内存:GAB,它是独有的
-
用于noscan类型小对象(noscan类型表示这个对象没有指针)
-
三个指针维护
-
Bump pointer风格对象分配
- 无需和其他分配请求互斥
- 分配动作简单高效
-
GAB对于go内存管理来说是一个大对象
本质:将多个小对象的分配合并成一次大对象的分配
问题:导致内存被延迟释放
方案:移动GAB中存活的对象
- gab总大小超过一定阈值时,将gab中存活的对象复制到另外分配的gab中
- 原先的gab可以释放,避免内存泄漏
- 本质:用copying GC的算法管理小对象
总结:分块、缓存、对象分配性能问题(小对象为主,分配路径过长)、Balanced GC
对象长度存放在哪里
用额外的gap,开头是1是对象头,go没有专门的对象头
三、编译器和静态分析
编译器的结构
- 系统软件
- 分析部分(前端)
- 综合部份(后端,本文主要关注点)
静态分析
- 不执行程序代码,推导程序的行为,分析程序的性质。
- 控制流:程序执行的流程
- 数据流:数据在控制流上的传递
发现性质,根据这些性质优化代码
过程内分析和过程间分析
-
内:仅在函数内部
-
间:过程调用时参数传递和返回值的数据流和控制流
过程间分析需要联合求解,比较复杂
四、Go编译器优化
-
why:
- 用户无感知
- 通用性优化
-
现状
- 采用的优化少
- 编译时间较短,没有进行较复杂的代码分析优化
-
优化思路
- 场景:面向后端长期执行任务
- Tradeoff:用编译时间换取更高效的机器码
函数内联
-
内联:将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反应参数的绑定
-
优点
- 消除函数调用开销
- 间变内,帮助其他优化,例如逃逸分析
-
函数内联多大程度影响性能
-
缺点
- 函数体变大
- 编译生成的Go镜像变大
-
内联在大多数情况下是正向优化
-
内联策略
- 调用被调用函数规模
- ...
Beast Mode
-
Go函数内联受到的限制较多
- 语言特性
- 内联策略保守
-
Beast mode:调整内联策略,使更多函数被内联
- 降低函数调用开销
- 增加了其他优化机会:逃逸分析
-
开销
- Go镜像增加:~10%
- 编译时间增加:更多的机会去做了优化
逃逸分析
-
分析代码中指针的动态作用域:指针在何处可以被访问
-
大致思路
-
beast mode:函数内联拓展了函数边界,更多对象不逃逸
-
优化:未逃逸的对象可以在栈上分配
- 减少heap上分配,降低GC负担
- 栈上分配回收很快:移动sp