Go高性能优化与实践 | 青训营笔记

219 阅读8分钟

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

GO-学习笔记

优化

  • 性能优化,提升软件系统处理能力,减少不必要的消耗,充分发掘计算算力
  1. 提升用户体验
  2. 资源高效利用,降低成本,提高效率
  3. 业务层、sdk、基础库、语言运行时、os系统
  4. 业务层,针对特定场景,具体问题具体分析
  5. 语言运行时,针对通用型问题,主要涉及内存管理和分配,资源调度
  • 开发自定义SDK,保证接口稳定,优化具体实现

内存管理

  • 动态内存,程序在运行时根据需求动态分配内存
  • 自动内存管理,程序运行时系统管理动态内存,避免手动内存管理,保证内存使用的正确性和安全性
  1. double-free,两次释放相同位置的内存
  2. use-after-free,使用没有开辟内存的位置
  • 三个任务
  1. 为新对象分配空间
  2. 找到存活对象
  3. 回收死亡对象的空间

相关概念

  • Mutator threads,业务线程,分配新对象,修改对象指向关系
  • Collector,GC线程,找到存活对象,回收死亡对象的内存空间
  • Serial GC,串行GC,只有一个GC线程,存在STW
  • Parallel GC,并行GC,多个GC线程,存在STW
  • Concurrent GC,与用户线程并发执行,并发回收,必须感知到对象指向关系的改变
  1. 三色标记法,存在浮动垃圾和漏标的问题,通过写屏障解决

评价GC

  • 安全性,不能回收存活对象
  • 吞吐量,1 -(GC时间 / 程序执行总时间),越高越好
  • 暂停时间,STW
  • 内存开销,GC元数据的开销

Tracing garbage collection

  • 追踪垃圾回收,可达性分析,JVM
  1. 标记根对象,静态变量、全局变量、常量、线程栈
  2. 找到可达对象,求指针指向关系的传递闭包
  3. 清理所有不可达对象

清理策略

  • 根据对象的生命周期,使用不同的标记和清理策略
  • Copying GC,复制算法,把存活对象,复制到另外的内存空间
  • Mark-sweep GC,标记清除算法,把死亡对象的内存空间标记为可分配
  • Mark-comparct GC,标记整理/压缩算法,移动并整理存活对象

分代GC

  • 大部分对象分配出来之后,很快就不使用了
  • 每个对象都有年龄,经过GC的次数
  • 分为年轻代和老年代,进行不同的管理策略
  • 年轻代
  1. 存活数量少,采用复制算法
  2. GC吞吐率高
  • 老年代
  1. 趋于长时间存活,采用标记清除或者标记整理

Reference counting

  • 引用计数,每一个对象都有与之关联的引用数目,大于0时活着,OC/Swift
  • 优点
  1. 内存管理操作,平摊到程序执行过程中
  2. 不需要了解runtime的实现细节,C++智能指针为引用计数的实现
  • 缺点
  1. 维护开销较大,通过原子操作,保证对引用计数操作的原子性和可见性
  2. 存在循环引用,弱引用来解决
  3. 增加内存开销,需要额外的空间存储引用数目
  4. 回收大范围引用图的内存时,存在STW

Go的内存管理及优化

分块

  • 提前将内存分块,根据对象大小,选择最合适的块返回
  1. 调用系统调用mmap(),向os,申请一块内存mcache,4MB
  2. 将内存划分为几大块,mspan,8KB,包含一块mark bits,保存该块中的某块内存是否被标记为空闲,标记清除算法
  3. 将大块mspan划分为特定大小的小块,用于对象分配
  • noscan mspan,分配不包含指针的对象,GC不需要扫描
  • scan mspan,分配包含指针的对象,GC需要扫描

缓存

  • 借鉴了TCmalloc,thread caching,对内存做了很多级不同的缓存

TC is short for thread caching

  • g goroutine 某个协程,绑定一个p,用于分配对象
  1. 每个p包含一个mcache用于快速分配
  2. 每个mcache管理一组mspan,当mcache中的mspan分配完毕,向mcentral申请未分配块的mspan
  3. mspan中没有分配的对象,会被缓存在mcentral,而不是立即释放归还os image.png

优化内存管理

  • 对象分配是非常高频的操作
  • 小对象占比比较高
  • Go分配对象耗时较长
  1. 分配路径 g -> m -> p -> mcache -> mspan -> memory block -> return pointer
  2. mallocgc对象分配函数,调用很频繁

balanced gc

  • 类似于Thread Local Allocation Buffer
  • 直接改Go的SDK
  • 每个用户 g绑定一大块内存,GAB goroutine allocation buffer
  1. 用于noscan类型的小内存分配,小于128B
  2. 额外空间记录对象的起始地址,标记对象头
  3. 如果用于scan类型,不好进行GC,其中一个对象活着,可能引用了很多对象,导致这块大内存无法被回收
  • 用三个指针维护GABbase/end/top image.png
  • 指针碰撞bump-pointer风格的对象分配,不会与其他协程上的分配产生互斥,分配简单高效 image.png
  • GAB对Go内存管理来说是一个大对象,本质将多个小对象的分配,合并成一次大对象的分配

会导致内存被延迟释放,GAB中只存在一个小对象,会导致这一大块内存无法被及时回收

  • 移动GAB中的存活对象
  1. GAB总大小超过一定阈值,小于一定阈值,将GAB存活的对象复制到另外分配的GAB中,让原先的GAB能够被回收
  2. GoGC最后一个阶段,修改原来指向该存活对象的指针,需要STW,再为该g生成一个GAB
  3. 释放原先的GAB,避免内存泄漏
  4. 复制算法管理小对象,复制到survivor GAB暂存,不属于任何一个g

编译器和静态分析

编译器结构

  • 识别符合语法和非法的程序,生成正确且高效的代码 image.png
  • 分析部分
  1. 词法分析,生成词素
  2. 语法分析,生成语法树
  3. 语义分析,收集类型信息,进行语义检查
  4. 中间代码生成,生成IR,intermediate representation 中间表示
  • 综合部分
  1. 代码优化,机器无关的优化,生成优化后的中间表示
  2. 代码生成,生成目标代码

静态分析

  • 生成执行更快,效率更高的机器码
  • 不执行程序代码,推到程序行为的结果,分析程序的性质,程序执行是否正确
  • Control flow,控制流,程序执行的流程,用Control-flow graph编排
  • Data flow,数据流,数据在控制流上的传递,数据执行过程中某个阶段的值

过程内和过程间分析

  • Intra-procedural analysis,过程内分析,函数内部分析
  • Inter-procedural analysis,过程间分析,函数调用时参数传递和返回值的数据流和控制流
  1. 数据流和控制流同时分析,才能推断执行顺序

Go编译器优化

  • 用户无感知,重新编译即可获得性能收益,通用性优化
  • 优化思路
  1. 面向后端长期执行的任务
  2. 用编译时间换取更高效的机器码
  3. Beast mode,函数内联,逃逸分析,默认栈大小调整,边界检查消除,循环展开

函数内联

  • Go内联策略很保守
  • 将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
  • 优点
  1. 消除函数调用的开销,例如传递参数、保存寄存器
  2. 将过程间分析转换为过程内分析,帮助其他优化(逃逸分析)
  • 缺点
  1. 函数体变大,instruction cache,icache,指令高速缓存不友好
  2. 编译生成的Go镜像变大
  • 大多数是正向优化,根据策略决定是否内联
  1. 根据调用和被调用函数的规模
  • Beast Mode
  1. 调整函数内联策略,降低函数调用开销,增加其他优化机会
  2. 开销,Go增加10%,编译时间增加

逃逸分析

  • 分析代码中指针的动态作用域,指针在何处可以被访问
  1. 从对象分配出发,沿着控制流,观察对象的数据流
  2. 逃逸成功,作为参数传递给其他函数,传递给全局变量,传递给其他goroutine,传递给已逃逸的指针指向的对象
  • Beast Mode
  1. 函数内联拓展了函数边界,更多对象不逃逸
  2. 对象在栈上进行分配,回收很快,只需移动栈寄存器 sp StackPointer;减少在堆上的分配,降低GC负担