[高性能Go语言发行版优化与落地实践|青训营笔记]

127 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 1 天

0.前言

  1. 性能优化层面:业务代码、sdk、基础库、语言运行时、os
    业务层优化:针对特定场景,收益大;
    语言运行时优化:解决更通用的性能问题
    用数据驱动优化:自动化性能分析工具-pprof、依靠数据而非猜测、首先优化最大瓶颈
  2. 性能优化与软件质量(软件工程手段)
    保证接口稳定的前提下改进具体实现
    测试用例:覆盖尽可能多的场景、方便回归
    文档:做了什么,没做什么,达到什么效果
    隔离:通过选项控制是否开启优化
    可观测:必要的日志输出

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降低,时延降低,内存使用降低