Go语言优化与落地实践 | 青训营笔记

129 阅读4分钟

这是我参与「第三届青训营-后端场」笔记创作活动的的第1篇笔记.

性能优化

业务层优化:针对特定场景代码优化,改进具体问题
sdk优化:tradeoffs,改进更通用的功能
数据驱动优化:pprof,依靠数据分析而非猜测,优化最大瓶颈
性能优化需要保证的:
保证接口稳定;使用尽可能多的测试用例;编写文档说明;可选择是否开启优化;必要的日志输出

一、自动内存管理

管理的是动态内存(malloc),程序运行时系统管理,保证正确和安全性

任务

为新对象分配空间,找到存活对象,回收死亡对象的空间

mutator:业务线程,分配新对象,修改指向关系
collector:GC线程,找存活回收死亡
serialGC:只有一个collector,暂停
parallelGC:支持多个collector同时回收,暂停
concurrentGC:mutators和collectors可以同时执行,不暂停把m改成c,但是注意c必须感知对象指向关系的改变,已标记存活的对象必须指向已标记的

评价GC算法

不能回收存活的对象
吞吐率:1-GC时间/总时间
暂停时间
内存开销

追踪垃圾回收

对象被回收的条件:指针指向关系不可达的对象
过程:根对象(静态变量、全局变量、常量、线程栈)->标记可达对象->存活对象原地整理(mark-compact)或复制到另外的空间(copying),清理不可达对象(mark-sweep)
分代generational GC:分年轻代(可以用copy)和老年代(用sweep)取决于经历过GC的次数 引用计数:引用数大于0对象才存活
优点:内存管理的操作被平摊到程序执行过程中;不需要了解runtime的细节,如C++智能指针
缺点:需要原子操作,开销大;无法回收环形;占用内存开销;回收时还是可能引发暂停

二、go内存管理及优化

GMP模型:

zhuanlan.zhihu.com/p/261807834
goroutine:独立执行单元,必须绑定到p才能调度执行,动态扩容。G结构体对应goroutine,存储堆栈、状态、任务函数
machine:OS线程,真正执行计算的资源
processor:决定最大可并行的G的数量,提供执行环境、内存分配状态、任务队列

管理

1.分块,在heap上分配内存
mmap()向OS申请一大块内存->划分成大块mspan->划分成小块,根据对象的大小选择:noscan mspan分配不包含指针的对象,GC不扫描;scan mspan包含,需要
2.缓存
每个p包含一个mcache管理一组mspan用于快速分配,满了后向mcentral申请新mspan;当mspan空,会缓存在mcentral中,而不是立刻还给OS

优化

对象分配非常高频,小对象多,分配路径长g->m->p->mcache->mspan->mcentral
方案:
balancedGC:将多个小对象分配合并成一次大对象分配,但是导致内存延迟释放
每个g绑定一大块内存(GAB),用于noscan小对象分配,直接移动指针分配内存。使用copying算法优化:GAB总大小超过阈值,将其中对象复制到另外GAB,原先的可以释放。

三、编译器和静态分析

编译器的功能:识别合法和非法程序,生成正确高效代码
前端分析部分:词法分析生成词素;语法分析生成语法树(AST);语义分析进行检查;中间代码生成IR
后端综合部分:优化IR;代码生成

静态分析:不执行只推导程序行为,分析程序性质。分析控制流or数据流
过程内分析:仅在函数内部进行分析
过程间分析:考虑函数调用,需要同时分析控制和数据流,比较复杂

四、go编译器优化

优点:对用户透明,通用性
现状:采用的优化少,没有进行复杂的分析
思路:后端长期执行任务优化,tradeoff延长编译时间,换取更高效机器码

函数内联

消除函数调用开销,过程间分析->过程内;函数体变大,编译生成go镜像变大

beast mode
因为go内联受到的限制多,策略比较保守;beast mode可以调整内联策略,降低函数调用开销,增加其他优化机会如逃逸分析

逃逸分析

分析代码中指针的动态作用域:指针在何处可以被访问
从对象分配处出发,沿着控制流,观察数据流
若指针p在当前作用域s:作为参数传递给其它函数 or 全局变量 or 其他goroutine or 已逃逸指针指向的对象,则已逃逸
beast mode
函数内敛拓展函数边界,更多对象不逃逸;未逃逸的对象可以在栈上分配,直接移动sp;减少heap上分配,降低GC负担