这是我参与「第五届青训营」伴学笔记创作活动的第 1 天
0.前言
- 性能优化层面:业务代码、sdk、基础库、语言运行时、os
业务层优化:针对特定场景,收益大;
语言运行时优化:解决更通用的性能问题
用数据驱动优化:自动化性能分析工具-pprof、依靠数据而非猜测、首先优化最大瓶颈 - 性能优化与软件质量(软件工程手段)
保证接口稳定的前提下改进具体实现
测试用例:覆盖尽可能多的场景、方便回归
文档:做了什么,没做什么,达到什么效果
隔离:通过选项控制是否开启优化
可观测:必要的日志输出
1.自动内存管理(垃圾回收)
1.1 相关概念
动态内存:运行时根据需求动态分配内存 自动内存管理:避免手动,专注于业务实现;保证内存使用的正确性和安全性
三个任务:为新对象分配空间、找到存活对象、回收死亡对象的内存空间
mutatoe:业务线程,分配新对象,修改新对象指向关系
collector:GC对象,找到存活对象,回收死亡对象的内存空间
serial GC:算法只有一个collector
parallel GC:支持多个collectors同时回收
concurrent GC:mutators和collectors可以同时执行,必须感知对象指向关系的改变
评价GC算法:不能回收存货对象(安全性);吞吐率(花在GC以外时间的比例);暂停时间(业务是否感知);内存开销(GC元数据开销)
两种GC技术:追踪垃圾回收、引用计数;
1.2 追踪垃圾回收
对象回收条件:指针指向关系不可达的对象
标记根对象:静态变量、全局变量、常量、线程栈等
标记:找到可达对象,从根对象出发,找到所有可达对象
清理-所有不可达对象:
将存活对象复制到另外内存空间(copy gc);
将死亡对象内存标记为可分配(mark-sweep collection);
移动并整理存活对象(原地整理 mark-compact GC)根据对象生命周期,选择不同标记和清理策略
1.3 分代GC
分代假说:很多对象分配出来后很快不再使用
对象年龄:经历过GC的次数
目的:对年轻和老年对象指定不同GC策略,降低整体内存管理开销,不同年龄对象处于HEAP不同区域
年轻代策略:copy gc;
老年代策略:mark-sweep collection;
1.4 引用计数
每个对象有一个关联的引用数目
存活条件:当且仅当引用数大于0
优点:内存管理操作平摊到程序执行中;不需要了解runtime实现细节:C++智能指针
缺点:引用计数维护开销大(原子操作);无法回收环形数据结构;引入内存开销;回收时依然可能引入暂停(在复杂的数据结构中)
2 GO内存管理和优化
2.1 go内存分配
分块思想
为对象在heap上分配内存;
提前将内存分块:mmap(),mspan(),再分为特定大小的小块
noscan mspan:分配不包含指针的对象,gc不需要扫描
scan mspan:分配包含指针的对象,gc需要扫描
根据对象大小,选择最合适的小块返回
缓存思想TCMalloc:thread caching
2.2 go内存管理优化
对象分配非常高频:每秒GB
小对象占比高
go内存分配比较耗时
2.3 字节SDK优化方案:Balanced GC
每个g绑定一大块内存(1kb)-GAB,用于noscan类型的小对象分配(小于128B),三个指针维护gab(base、top、end)
指针碰撞:无需和其他分配请求互斥 GAB对于GO内存管理为大对象,将多个小对象分配合并为一次大对象的分配
分配方式导致内存被延迟释放-方案
gab总大小超过一定阈值时,将存活对象复制到另外分配的 gab中,原gab可以释放,用copy gc的算法管理小对象(本质)
性能:cpu usage降低,核心接口时延降低
3 编译器和静态分析
3.1 编译器结构
识别符合语法和非法的程序、生成正确且高效的代码
分析部分(前端):词法分析、语法分析、语义分析、中间代码生成
综合部分(后端):代码优化、代码生成
3.2 静态分析(后端优化的工具)
不执行程序代码,推导程序行为,分析程序性质
控制流分析:程序执行流程
数据流分析:数据在控制流上的传递
分析控制流和数据流,知道程序性质,根据性质优化代码
3.3 过程内和过程见分析
过程内分析:尽在函数内部进行分析
过程间分析:过程调用时参数传递和返回值的数据流和控制流(需要同时分析数据和控制流)
4 go编译器优化
动机:用户无感知即可获得性能收益;通用性优化
现状:采用的优化少,编译时间较短
优化思路:面向后端长期执行的任务,用编译时间换取高效机器码
beast mode:函数内联、逃逸分析、默认栈大小调整、边界检查消除、循环展开
4.1 函数内敛
将被调用函数体的副本替换到调用位置,重写代码反应参数绑定
优点:消除调用开销;过程间分析转为过程内分析
缺点:函数体变大;编译生成的go镜像变大
内敛大多数情况下为正向优化
内敛策略决定是否内敛:调用和被调函数规模
beast mode
go函数内敛受到较多限制
beast mode:调整函数内敛的策略,使更多函数被内敛
开销:go镜像增加、编译时间增加
4.2 逃逸分析
分析代码中指针的动态作用域,指针在何处可以被访问
大致思路:从分配处出发,沿着控制流观察对象的数据流;若发现指针p在当前作用域s(作为参数传递给其他函数、传给全局变量、传递给其他的goroutine、传递给已逃逸的指针指向的对象);则指针p指向的对象逃逸出s,否则没逃逸出s
beast mode:函数内敛拓展函数边界,更多对象不逃逸
优化:未逃逸对象在栈上分配(移动sp快,降低GC负担)
beast mode收益:cpu usage降低,时延降低,内存使用降低