go语言高性能优化与实践 | 青训营笔记

305 阅读5分钟

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

自动内存管理

自动内存管理在目的上有三个任务:1.为新对象分配空间,2.找到存货对象,3.回收死亡对象的内存空间。下面介绍自动内存管理实现这些目的的一些方法与概念

动态内存

  • 程序在运行时根据需求动态分配的内存
  • 自动内存管理(垃圾回收):由程序语言的运行时系统回收动态内存
    • 避免手动管理内存,专注实现业务逻辑
    • 确保内存使用的正确性和安全性
  • Mutator :业务线程,作用是分配新对象,修改对象指向关系
  • Collector:GC线程,作用是找到存活对象,回收死亡对象的内存空间。其中GC又可分为三种:
    • Serial GC:只有一个collector
    • Parallel GC:支持多个collectors同时回收的GC算法
    • Concurrent GC:业务线程和GC线程可以同时执行,在具体实现上Collectors必须感知对象指向关系的改变

追踪垃圾回收

在程序中,当一个对象在指针指向关系中不可达时,它将被回收。在具体执行过程中,根据对象的生命周期,使用不同的标记和清理策略GC

  • Copying GC:将对象复制到另外的内存空间

image.png

  • Mark-sweeping GC:将可回收的对象的标记,使用free list管理空闲内存

image.png

  • Compact GC:原地整理对象:

image.png

分代 GC(Generational GC)

在实际开发中我们可以发现,有很多对象在分配出来后很快就不再使用了,由此提出了分代假说:把每个变量都视为有年龄,年龄的大小为经历过GC的次数,针对年轻和年老的对象制定不同的GC策略,降低整体内存管理的开销,不同的年龄的对象处于heap的不同区域。

  • 年轻代
    • 常规的对象分配策略
    • 由于存活的对象很少,可以采用copying collection
  • 老年代
    • 对象一直趋于活着,反复赋值开销较大,可以采用mark-sweep collection

引用计数

我们可以给每个对象都记录下与之关联的引用数目,称为引用数,只有当对象的引用数大于0时,我们才认为这个对象是存活的,如图

image.png

这么做的优点是内存管理的操作被平摊到程序执行过程中,内存管理不需要了解runtime的实现细节。缺点也比较明显:维护引用计数的开销较大,因为我们必须通过原子操作保证对引用计数的原子性和可见性,且这种方法无法解决循环引用的问题:

image.png

Go 内存管理及其优化

内存分配

为了给对象在内存上分配内存,Go语言的策略是提前将内存分块

  1. 调用系统调用mmap()向操作系统申请一大块内存,例如4MB
  2. 先将申请的内存继续分成大块,例如8KB,称作mspan,可分为2种:
    • noscan msapn:分配不包含指针的对象,GC无需扫描
    • scan msapn:分配包含指针的对象,GC需要扫描
  3. 继续将大块划分成特定大小的小块,根据对象的大小,选择最合适的块返回给对象分配内存

优化方案——Balanced GC

给每个goroutine都绑定一大块内存,称作goroutine allocation buffer(GAB),每个GAB用于noscan类型的小对象分配(小于128B),同时使用三个指针维护GAB:base,end和top

image.png

GAB对于Go内存管理就是一个大对象,这种方案的本质是将多个小对象的分配合并成一次大对象的分配,因此无需和其他分配请求互斥,分配动作简单高效

但带来的问题是导致内存被延迟释放,只有GAB中还有对象存活,那么不管这个对象占用的空间有多少,整个GAB都不能被释放,一个解决方案是移动GAB中存活的对象:当GAB总大小超过一定阈值时,把GAB中存活的对象复制到另外分配的GAB中,而原来的GAB可以释放,避免内存泄漏。本质是用前面提到的copying GC算法管理小对象

image.png

编译器和静态分析

编译器的结构

  • 分析部分(前端 front end)
    • 词法分析,生成词素
    • 语法分析,生成语法树
    • 语义分析,收集类型信息,进行语义检查
    • 中间代码生成,生成IR
  • 综合部分(后端back end)
    • 代码优化,机器无关化优化,生成优化后的IR

    • 代码生成,生成目标代码

image.png

过程内分析和过程间分析

过程内分析指尽在函数内部分析,过程间分析需要考虑过程调用时参数传递和返回值的数据流和控制流,所以一种优化思路就是——函数内联:

  • 将被调用的函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定。 这样做可以消除例如参数传递,保存寄存器等函数调用的开销,更关键的的是将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析。这当然也会导致函数体变大和编译生成的Go镜像也变大,但在大多情况下函数内联是正向优化。