这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记
4. 高性能Go语言发行版优化与落地实践
从性能优化的角度来看我们应用的结构,从上到下可将其分成5个层面:业务代码、SDK、基础库、语言运行时(runtime)、OS
业务代码:根据需求写的代码
SDK、基础库:提供了一些api、高级数据结构等
runtime:垃圾回收机制、调度器、编译器等。本章主要讲runtime优化
OS:提供隔离的运行环境
4.1 自动内存管理(垃圾回收)
4.1.1 相关概念
动态内存:程序运行时根据需求动态分配的内存,如malloc
自动内存管理(垃圾回收):程序运行时系统自动回收动态内存
自动内存管理要做什么:为新对象分配空间、找到存活对象、回收死亡对象的内存空间
按工作任务来分:
Mutator:业务线程。用于分配新对象,修改对象指向关系
Collector:GC 线程。找到存活对象,回收死亡对象的内存空间
按运行算法来分:
Serial GC:只有一个collector,且collector执行时其他mutator都需要暂停
Parallel GC:并行 GC,支持多个collector同时工作,且collector执行时其他mutator都需要暂停
Concurrent GC:并发 GC,支持多个collector同时工作,collector和mutator可以同时工作
常用的GC技术:
Tracing garbage collection 追踪垃圾回收
Reference counting 引用计数
4.1.2 Tracing garbage collection 追踪垃圾回收
该技术的思路:清理指针指向关系不可达的对象
步骤:
1、标记根对象:如静态变量、全局变量、常量、线程栈等
2、标记可达对象:从根对象出发,找到所有可达对象
3、清理不可达对象
策略1 Copying GC:将存活对象复制并整理好连续存放在另外的内存空间,则当前的内存块就完全空了可用了
策略2 Mark-sweep GC:将死亡对象的内存标记为可分配
策略3 Mark-compact GC:在当前内存块中整理好存活对象,让它们连续存放
那我们应该采用哪种策略来清理死亡对象呢?我们可以采用Generational GC 分代 GC方式来选择策略
4.1.3 Generational GC 分代 GC
原理:大部分对象可能因为仅存在于某个函数当中,因此分配内存后很快就不再用了
概念:我们称这个对象经历过GC的次数为它的年龄,根据年龄分成年轻代、老年代
年轻代:存活对象数很少,因此使用策略1
老年代:存活对象趋于一直活着,因此使用策略2
4.1.4 Reference counting 引用计数
每个对象都会有一个数字表示它被其他对象引用的数目,当一个对象的被引用数>0,则该对象是存活的
优点:
在程序执行的过程中可以顺带完成内存管理
解耦合,内存管理不需了解runtime的实现细节
缺点:
在多线程编程中,须通过原子操作来维护被引用数,开销较大
记录被引用数需要额外内存开销
回收内存时可能引发暂停
4.2 Go 内存管理及优化
4.2.1 Go的内存分配法1--分块
若现在用go代码初始化了一个对象,那么go底层是如何为这个对象分配内存的呢?
目标:go会在heap中为对象分配内存
步骤:在业务执行前,go就会先用mmap向OS申请一大块内存如4MB;然后将这块内存划分为若干个mspan,如8KB;再将mspan划分成若干个noscan mspan用于分配不包含指针的对象、scan mspan用于分配包含指针的对象,如8B、16B等等。业务方法中初始化对象,go则根据对象大小选择最适合的noscan/scan mspan进行分配
4.2.2 Go的内存分配法2--缓存
一个 Goroutine 的运行需要G+P+M三部分结合起来
G: Goroutine 执行的上下文环境。
M: 操作系统线程。
P: Processer。进程调度的关键,调度器,也可以认为约等于CPU。
go的内存分配器借鉴了TCMalloc内存分配的思路,也对内存做了很多级不同的缓存,加快内存分配速度
特点:
每个p包含一个mcache用于快速分配,给这个p绑定的g分配对象
mcache管理一个mspan
当mcache的mspan分配完了,则向mcentral申请一组有空余块的mspan
当mcache的mspan完全空闲,mspan会被缓存在mcentral中,而不是立即释放归还给OS
4.2.3 Go的自动内存管理过程
go内存会分成堆区(Heap)和栈区(Stack)两个部分,程序在运行期间可以主动从堆区申请内存空间,这些内存由内存分配器分配并由垃圾收集器负责回收。栈区的内存由编译器自动进行分配和释放,栈区中存储着函数的参数以及局部变量,它们会随着函数的创建而创建,函数的返回而销毁。如果只申请和分配内存,内存终将枯竭。Go使用垃圾回收收集不再使用的span,把span释放交给mheap,mheap对span进行span的合并,把合并后的span加入scav树中,等待再分配内存时,由mheap进行内存再分配。因此,Go堆是Go垃圾收集器管理的主要区域。
4.2.4 Go的内存回收技术
Tracing garbage collection 追踪垃圾回收
4.2.5 Go内存分配的问题
对象分配十分高频
小对象占比高
Go内存分配比较耗时,因为分配路径较长:g->m->p->mcache->mspan->memory block->return pointer
4.2.6 字节的优化方案:Balance GC
问题:若一个GAB中只有少数noscan mspan是存活的,会导致整块GAB被延迟释放
解决方法:
4.3 编译器和静态分析
4.3.1 编译器的结构
4.3.2 静态分析
4.3.3 过程内分析、过程间分析
4.4 Go 编译器优化
4.4.1 函数内联
4.4.2 字节的优化方案:Beast Mode
是字节对golang的sdk进行优化的一项技术
4.4.3 逃逸分析
p若在s以外被访问到,则说明p逃逸了
\