Go内存管理学习| 青训营笔记

46 阅读4分钟

这是我参与「第五届青训营 」笔记创作活动的第5天

性能优化

什么是性能优化?

提升软件系统处理能力,减少不必要的消耗

为什么做性能优化?

带来用户体验的提升;资源的高效利用也能够降低成本提高效率

性能优化层面

从上到下为

  • 业务代码
    • 针对特定场景,具体问题,具体分析
    • 容易获得较大性能收益
  • SDK
  • 基础库
  • 语言运行时
    • 解决更通用的性能问题
    • 考虑更多场景
    • Tradeoffs
  • OS

数据驱动进行优化是关键

  • 自动化性能分析工具——pprof
  • 依靠数据而非猜测
  • 首先优化最大瓶颈

自动内存管理

是由程序语言的运行时系统管理动态内存,避免了手动内存管理,专注于实现业务逻辑。保证内存使用的正确性和安全性。

GC基本概念

  • Mutator:业务线程,分配新对象,修改对象指向关系
  • Collector:GC线程,找到存活对象,回收死亡对象的内存空间
  • Serial GC:只有一个collector
    • 运行时暂停所有Mutator,执行一个collector
  • Parallel GC:支持多个collector同时回收的GC算法
  • Concurrent GC:mutator和collector可以同时执行
    • Collector必须感知对象指向关系的改变

image.png

评价GC算法的要点

安全性,吞吐率,暂停时间,内存开销

追踪垃圾回收

对象被回收的条件:指针指向关系不可达的对象

回收被分为三步骤:

  • 标记跟对象
    • 静态变量,全局变量,常量,线程栈
  • 标记:找到可达对象
    • 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
  • 清理:所有不可达对象
    • 将存活对象复制到另外的内存空间
    • 将死亡对象的内存标记为可分配
    • 移动并整理存活对象
  • 清理:所有不可达对象 生命周期生命周生命周期 根据对象的声明周期,使用不同的标记和清理策略

分代GC

  • 每个对象都有年龄:年龄即经过GC的次数
  • 目的:对于年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
  • 不同年龄的对象处于heap的不同区域

年轻代

  • 常规对象分配
  • 由于存活对象很少,可以采用copying collection
  • GC吞吐率很高

老年代

  • 对象趋于一直活着,反复复制开销较大
  • 可以采用 mark-sweep collection

G内存管理及优化

Go内存分配

Go会提前将内存分块,在需要分配时返回一块大小最接近的一块

  • 调用系统调用mmap(),向OS申请一大块内存,例如4MB
  • 先将内存划分成大块,例如8KB,称作mspan
  • 再将大块继续划分成特定的小块,用于对象分配
  • noscan mspan:分配不包含指针的对象----GC不需要扫描
  • scan mspan:包含只针对对象----GC需要扫描
  • 由于是很高操作,所以会比较占用cpu

缓存

TCMalloc:thead caching

  • Go语言采用GMP模式
  • 每个p包含一个mcache用于快速分配,用于为绑定于p上的g分配对象
  • mcache 管理一组mspan
  • 当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
  • 当mspan中没有分配的对象时,mspan会被缓存在mcentral中,而不是立刻释放并归还给OS

内存分配路径

g -> m -> p -> mcache ->mspan ->memory block -> return pointer

优化方案

Balanced GC

  • 每个g都绑定一大块内存(1KB),称作goroutine allocation buffer
  • GAB用于noscan类型的小对象分配: < 128B
  • 使用三个指针维护GAB
  • Bump Pointer(指针碰撞)风格对象分配

收益:高峰期能降低CPU使用率4.6%,核心接口时延下降4.5%~7.7%

编译器和静态分析

编译器结构

@`R[ED%RV2L]WGPRC6OSX{V.png

静态分析

  • 不执行程序代码,推导程序的行为
  • 控制流:推导程序执行的流程
  • 数据流:数据在控制流上的传递
  • 通过今天静态分析我们可以知道更多关于程序的内容
  • 过程内分析:仅在函数内部进行分析
  • 过程间分析:考虑过程调用参数传递和返回值的数据流和控制流

Go编译器优化

函数内联

就是把将被调用的函数体callee的副本替换到调用的位置上,同时重写代码以反映参数的绑定

优点:

  • 消除函数调用开销
  • 将过程间分析转为过程内分析,帮助其他优化

缺点:

  • 函数体变大
  • 编译生成的Go镜像变大

逃逸分析

分析代码中指针的动态作用域:指针在何处可以被访问 大致思路:

  • 从对象分配处出发,沿着控制流,观察对象的数据流
  • 若发现指针p在当前作用域s:
    • 作为参数传递给其他函数
    • 传递给全局变量
    • 传递给其他goroutine
    • 传递给已经逃逸的指针指向对象