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

146 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第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:在当前内存块中整理好存活对象,让它们连续存放

image-20220525015609549

那我们应该采用哪种策略来清理死亡对象呢?我们可以采用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

image-20220525151004831

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

image-20220525171150014

问题:若一个GAB中只有少数noscan mspan是存活的,会导致整块GAB被延迟释放

image-20220525171202751

解决方法:

image-20220525171937483

4.3 编译器和静态分析

4.3.1 编译器的结构

image-20220525200635035

4.3.2 静态分析

image-20220525201157476

4.3.3 过程内分析、过程间分析

image-20220525201837064

4.4 Go 编译器优化

image-20220525202130028

4.4.1 函数内联

image-20220525204213599

4.4.2 字节的优化方案:Beast Mode

是字节对golang的sdk进行优化的一项技术

image-20220525205103071

4.4.3 逃逸分析

p若在s以外被访问到,则说明p逃逸了

image-20220525205230134

\