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

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

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

本节主要内容:
image-20220602175432811

前言

image-20220602174456001

image-20220602174913877

运行时主要是指SDK。

image-20220602175306045
尽量以测试驱动开发。

自动内存管理

概念

image-20220602175722028

double-free:一块儿内存被释放后又被释放了一遍。
use-after-free:一块儿内存被释放后又被使用了。
这两个问题不仅仅会带来正确性的问题,程序很可能就crash了,甚至会带来一些安全漏洞。在CVE库上搜索这两个关键词会有很多漏洞。

GC三个任务:

  • 为新对象分配空间

  • 找到存活对象

  • 回收死亡对象的内存空间

image-20220602181739735
这里Serial GC和Parallel GC都会暂停业务,而Concurrent GC可以让业务线程以及GC线程同时运行,当然这也会带来问题:
image-20220602181905862
如果GC将o和a标记为存活对象后,o又指向了b,而b未被标记为存活对象,因此有可能被清除。
因此,Collectors必须感知对象指向关系的改变!

image-20220602182046854

Tracing garbage collection(追踪垃圾回收)

image-20220602182547308

回收一共分为了三步:

  1. 在全局变量和栈中标记根对象,即1,2,3.
  2. 通过根对象找到并标记可达对象,即4,5.
  3. 清理不可达对象,即6,7.这里分了三种方法:
    • Copying GC:将存活的对象复制到另外的内存空间,然后当前内存空间就可以继续使用随意分配了。
      image-20220602183002439
    • Mark-sweep GC:将死亡对象的内存标记为 可分配,组成一个链表,下次分配对象的时候可以直接使用free list中的空间
      image-20220602183017599
    • Mark-compact GC:原地整理对象,compact即压缩。
      image-20220602183049127

有这么多GC策略,我们该如何挑选?
根据对象的生命周期,使用不同的标记和清理策略。

Generational GC(分代GC)

image-20220602185729534
分代假说将对象分为了年轻代和老年代。
例如一个函数执行过程中创建了很多对象,但当返回结果后函数内的大部分对象都是应当被销毁的。因此很多对象在分配出来后很快就不再使用了。
年轻代和老年代的区分就是通过经历过多少次GC,经历的次数越多还活着说明越老越重要,因此可以分开管理,并用不同的GC策略。

image-20220602190035756年轻代使用Copying GC,是因为当前内存中可能90%的对象都应当销毁,只有小部分对象存活,只需要把这一小部分对象移动到另一块儿内存即可。
老年代使用Mark-sweep GC,因为他们大多数都会继续活着,只有少部分会被销毁,因此如果用Copying GC的话需要拷贝90%的对象,消耗很大,而Mark-sweep GC只需要他们留在原地即可。

Reference counting(引用计数)

image-20220602190425890
左上角红框中,2是指有两个箭头指向它,1代表一个对象指向它,也就是被引用了一次。
右下角红框的0,因为没有对象引用它,所以计数是0。

image-20220602200933262

Go内存管理及优化

Go内存分配

分块

image-20220602201433195

缓存

image-20220602201539469
GO语言GC借鉴了TCMalloc,可以根据右边的图来梳理分配过程。
Go 内存管理构成了多级缓存机制,从 OS 分配得的内存被内存管理回收后,也不会立刻归还给 OS,而是在 Go runtime 内部先缓存起来,从而避免频繁向 OS 申请内存。内存分配的路线图如上。

GO内存管理优化

image-20220602202342000
image-20220602202355783
pprof可以看到分配函数占比很高。

优化方案:Balanced GC

image-20220602202628852
这里虽然很好,通过对GAB的一次分配,省略了很多小对象的长的分配路径,直接从GAB里调整top指针位置即可(指针碰撞的方式),但是因为小对象生命周期较短,很快都死掉了,但GAB作为一个大对象,只要里面有一个小对象存活GAB就会被认为是存活的,从而不会被回收。

image-20220602203403466这里通过copying GC的算法解决了这个问题,当总的GAB大小到了一定程度,就会对GAB进行一次合并,减少一个小对象让整个GAB不能被回收的问题。

性能收益:
image-20220602203545871
下面那个是开启Balanced GC后的曲线。

编译器和静态分析

基本介绍

编译器的结构:

image-20220602204118058

静态分析

image-20220602204515367
静态分析左上的图,控制流图CFG右侧的图,数据流左下的图,经过这一系列的分析我们可以将这部分代码用return 4替代,完成优化,省去了那一坨代码的生成运算过程。

过程内和过程间分析

image-20220602204844660

Go编译器优化

image-20220602205029719
因为想要优化获得更高效的机器码需要花时间去分析代码,然后会使编译时间变长,而GO语言本身编译时间较短,没必要优化。
字节为了让自己的后端长期执行的任务更加高效,创建了Beast mode模式,里面用了很多优化技术,这里主要讲前两个。

函数内敛

image-20220602210016125
image-20220602210027765
函数内敛优化非常的明显。image-20220602210050405
递归的时候或者函数体本身已经很大了就不要函数内敛了,因此也需要设计一些内敛的策略。

Beast Mode

字节自己用来搞优化的一个产品。

image-20220602210317507

image-20220602210841391

逃逸分析

image-20220602210519704
即如果这个指针传递出去了,就可以被外界访问到,那么就会给GC压力。而上一步的函数内敛让更多的对象不再逃逸,因此可以在栈上更加快的分配,也减少了GC的负担。

总结

image-20220602210858775

posted @ 2022-06-02 21:12  杀戒之声  阅读(66)  评论(0编辑  收藏  举报