Go 语言内存管理与优化 | 青训营笔记

129 阅读7分钟

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

性能优化

基本问题

    性能优化是什么
        - 提升软件系统处理能力,减少不必要的消耗
    
    为什么要做
        - 提升用户体验
        - 降低成本,高效利用资源

两个层面

    业务层优化
        - 针对特定场景的具体问题
        - 这种优化可以获得较大的性能收益
    
    语言运行时优化
        - 面对更加通用的性能问题
        - 需要考虑更多的场景
        - 需要做更多的利弊权衡

可维护性

    1. 需要保证接口的稳定才能进行改进
    2. 需要使用尽可能多的测试用例覆盖更多的场景
    3. 需要清晰的文档进行描述
    4. 需要对优化进行隔离
    5. 需要有必要的日志输出

内存管理与优化

自动内存管理

  1. 基本概念

     动态内存
         程序运行时动态分配的内存
     
     自动内存管理(又称垃圾回收)
         - 程序语言的运行时系统自动管理动态内存
         - 避免手动内存管理,使程序员可以专注于实现业务逻辑
         - 保证了内存使用的正确与安全
     
     自动内存管理的三个任务
         - 为新对象分配空间
         - 找到存活对象
         - 回收死亡对象的内存空间
     
     相关术语
         - Mutator: 业务线程,用户启动的线程,分配新对象,修改对象指向关系
         - Collector: `GC`线程,找到存活对象,回收死亡对象的内存空间
         - Serial GC: 只有一个`collector`执行回收的GC操作,此时`mutator`处于暂停状态
         - Parallel GC: 并行`GC`,支持多个`collectors`同时执行回收的`GC`操作,此时`mutator`处于暂停状态
         - Concurrent GC: 并发`GC`,支持`mutator(s)``collector(s)`同时执行的`GC`算法
     
     GC算法的评价指标
         - 安全性: 不能够取回收存活的对象,这是最基本的要求
         - 吞吐率: 除去GC花费的时间后剩余时间占程序执行总时间的比例
         - 暂停时间: 执行垃圾回收时,将业务暂停的时间,牵涉到业务是否对GC有感知
         - 内存开销: GC元数据占用的内存开销
    
  2. 追踪垃圾回收

     对象被回收的条件
         指针指向关系不可达的对象
     
     步骤:
         - 标记根对象
             包括静态变量、全局变量、常量、线程栈等
         - 找到并标记可达对象
             求指针指向关系的传递闭包,即从根对象出发,找到所有可达对象
         - 清理所有不可达对象
             - 有三种策略,需要根据对象的生命周期使用不同的标记和清理策略
             - 第一种: 将存活对象直接复制到另外的内存空间,原内存空间直接清空
             - 第二种: 将死亡对象的内存标记为可分配,之后可以直接进行分配
             - 第三种: 移动并整理存活对象,将存活对象移动到一个特定位置,如开头。之后分配内存时从特定位置附近开始分配,如紧跟之后进行分配
    
  3. 分代GC

     基于分代假说而设立
     
     前提
         假设很多对象在分配出来后很快就不再使用了
     
     观点
         每个对象都有年龄,即经历过GC的次数
     
     目的
         对年轻和老年的对象,知道不同的GC策略,降低整体内存管理的开销
     
     不同年龄的对象处于不同的区域
         - 年轻代
             - 常规的对象分配
             - 存活对象少,使用前文提到的第一种清理策略
             - GC吞吐率高
         - 老年代
             - 对象趋向于一直活着,反复复制开销大
             - 使用前文提到的第二种清理策略
             - 碎片过多时,使用第三种清理策略进行整理
    
  4. 引用计数

     每个对象都有一个相关联的引用数目
     
     存活条件
         引用计数大于0
         
     优点
         - 内存管理操作被平坦到程序执行过程中
         - 内存管理不需要了解运行时的实现细节
     
     缺点
         - 维护引用计数开销大,需要通过原子操作保证原子性,开销较大
         - 环形结构无法回收
         - 需要引入额外的内存空间,存在一定的内存开销
         - 回收较大的数据结构时,难以避免会引发暂停
    

Go的内存分配

  1. Go内存分配 - 分块

     目的
         在堆上为对象分配内存
     
     做法
         提前将内存分块
     
     操作步骤
         - 调用系统使用`mmap()`向OS申请一大块内存
         - 将内存划分成大块,称为mspan
         - 再将大块划分为特定大小的小块,用于内存分配
     
     mspan分为两种
         - noscan mspan: 分配不包含指针的对象,即GC不需要扫描
         - scan mspan: 分配包含指针的对象,即GC需要扫描
     
     对象分配时
         根据对象大小,在mspan中,选择最合适的小块返回
    
  2. Go内存分配 - 缓存

     目的
         在堆上为对象分配内存
     
     做法
         维护一个mcache用于快速分配,在mcache中管理一组mspan用于分配,不足时向下一级缓存申请mspan。
     
     操作步骤
         - 首先通过协程找到她所属的p,并通过p找到mcache
         - 当mcache中存在未分配的块,即mspan未分配完毕时,直接返回。
         - 当mcache中不存在未分配的块,即mspan分配完毕时,mspan向下级缓存,即mcentral申请带有未分配块的mspan。
         - 当mcache的一组mspan中所有对象都已经释放,此时这一组mspan会归还给下一级缓存,即mcentral中。并不会立刻归还给OS,下一级缓存会按照一定的策略将mspan归还给OS。
    

Go的内存管理优化

  1. 问题

     内存分配非常高频
         
     小对象占比高
         
     Go内存分配比较耗时,分配路径长,操作频繁
    
  2. 优化方案

     Balanced GC
     
     操作步骤
         - 为每一个协程分配一大块内存,称为GAB
         - GAB用于noscan的小对象的分配
         - 使用三个指针维护GAB
         - 只需要进行指针操作即可完成内存分配
     
     本质
         将多个小对象的分配合并成一次大对象的分配,减少内存分配次数
     
     问题
         GAB的对象分配方式会导致内存被延迟释放,比如GAB中仅有一个小对象存活时,整个GAB都是存活的,从而导致了内存的延迟释放
     
     解决方案
         当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中,原先的GAB就可以进行释放,防止内存泄漏。
    

编译器

编译器与静态分析

  1. 编译器

     识别符合语法的程序,并通过词法、语法、语义分析和代码优化进而生成可执行的二进制文件
    
  2. 静态分析

     不执行程序代码,通过控制流分析、数据流分析的方法推导程序的行为,进而分析程序的性质,从而优化程序
    

编译器优化

  1. 函数内联

     将调用函数的函数体副本替换到调用位置上,同时重写代码以反映参数的绑定
     
     优点
         消除了函数调用开销
         将过程间分析转化为过程内分析,帮助其它优化
     
     缺点
         函数体变大
         编译生成的Go镜像变大
    
  2. Beast Mode

     调整函数内联策略,使更多函数被内联
    
  3. 逃逸分析

     分析代码中指针的动态作用域,即指针在何处可以被访问
     
     优化
         未逃逸对象可以在栈上分配,此时分配和回收都很快,而且可以减少堆上的分配,降低GC负担
    

个人总结

    本次课程中接触到了一些性能优化的思路,主要针对内存的优化和编译器的优化。同时这些优化思路在其它语言的优化中也可以提供借鉴作用。

引用

  • 字节内部课: 高质量编程与性能调优实战