高性能 Go 语言发行版优化与落地实践(四)|青训营笔记
高性能 Go 语言发行版优化与落地实践(四)|青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第四篇笔记。
本节主要内容:
前言
运行时主要是指SDK。
尽量以测试驱动开发。
自动内存管理
概念
double-free:一块儿内存被释放后又被释放了一遍。
use-after-free:一块儿内存被释放后又被使用了。
这两个问题不仅仅会带来正确性的问题,程序很可能就crash了,甚至会带来一些安全漏洞。在CVE库上搜索这两个关键词会有很多漏洞。
GC三个任务:
-
为新对象分配空间
-
找到存活对象
-
回收死亡对象的内存空间
这里Serial GC和Parallel GC都会暂停业务,而Concurrent GC可以让业务线程以及GC线程同时运行,当然这也会带来问题:
如果GC将o和a标记为存活对象后,o又指向了b,而b未被标记为存活对象,因此有可能被清除。
因此,Collectors必须感知对象指向关系的改变!
Tracing garbage collection(追踪垃圾回收)
回收一共分为了三步:
- 在全局变量和栈中标记根对象,即1,2,3.
- 通过根对象找到并标记可达对象,即4,5.
- 清理不可达对象,即6,7.这里分了三种方法:
- Copying GC:将存活的对象复制到另外的内存空间,然后当前内存空间就可以继续使用随意分配了。
- Mark-sweep GC:将死亡对象的内存标记为 可分配,组成一个链表,下次分配对象的时候可以直接使用free list中的空间
- Mark-compact GC:原地整理对象,compact即压缩。
- Copying GC:将存活的对象复制到另外的内存空间,然后当前内存空间就可以继续使用随意分配了。
有这么多GC策略,我们该如何挑选?
根据对象的生命周期,使用不同的标记和清理策略。
Generational GC(分代GC)
分代假说将对象分为了年轻代和老年代。
例如一个函数执行过程中创建了很多对象,但当返回结果后函数内的大部分对象都是应当被销毁的。因此很多对象在分配出来后很快就不再使用了。
年轻代和老年代的区分就是通过经历过多少次GC,经历的次数越多还活着说明越老越重要,因此可以分开管理,并用不同的GC策略。
年轻代使用Copying GC,是因为当前内存中可能90%的对象都应当销毁,只有小部分对象存活,只需要把这一小部分对象移动到另一块儿内存即可。
老年代使用Mark-sweep GC,因为他们大多数都会继续活着,只有少部分会被销毁,因此如果用Copying GC的话需要拷贝90%的对象,消耗很大,而Mark-sweep GC只需要他们留在原地即可。
Reference counting(引用计数)
左上角红框中,2是指有两个箭头指向它,1代表一个对象指向它,也就是被引用了一次。
右下角红框的0,因为没有对象引用它,所以计数是0。
Go内存管理及优化
Go内存分配
分块
缓存
GO语言GC借鉴了TCMalloc,可以根据右边的图来梳理分配过程。
Go 内存管理构成了多级缓存机制,从 OS 分配得的内存被内存管理回收后,也不会立刻归还给 OS,而是在 Go runtime 内部先缓存起来,从而避免频繁向 OS 申请内存。内存分配的路线图如上。
GO内存管理优化
pprof可以看到分配函数占比很高。
优化方案:Balanced GC
这里虽然很好,通过对GAB的一次分配,省略了很多小对象的长的分配路径,直接从GAB里调整top指针位置即可(指针碰撞的方式),但是因为小对象生命周期较短,很快都死掉了,但GAB作为一个大对象,只要里面有一个小对象存活GAB就会被认为是存活的,从而不会被回收。
这里通过copying GC的算法解决了这个问题,当总的GAB大小到了一定程度,就会对GAB进行一次合并,减少一个小对象让整个GAB不能被回收的问题。
性能收益:
下面那个是开启Balanced GC后的曲线。
编译器和静态分析
基本介绍
编译器的结构:
静态分析
静态分析左上的图,控制流图CFG右侧的图,数据流左下的图,经过这一系列的分析我们可以将这部分代码用return 4替代,完成优化,省去了那一坨代码的生成运算过程。
过程内和过程间分析
Go编译器优化
因为想要优化获得更高效的机器码需要花时间去分析代码,然后会使编译时间变长,而GO语言本身编译时间较短,没必要优化。
字节为了让自己的后端长期执行的任务更加高效,创建了Beast mode模式,里面用了很多优化技术,这里主要讲前两个。
函数内敛
函数内敛优化非常的明显。
递归的时候或者函数体本身已经很大了就不要函数内敛了,因此也需要设计一些内敛的策略。
Beast Mode
字节自己用来搞优化的一个产品。
逃逸分析
即如果这个指针传递出去了,就可以被外界访问到,那么就会给GC压力。而上一步的函数内敛让更多的对象不再逃逸,因此可以在栈上更加快的分配,也减少了GC的负担。