后端基础第四天 | 青训营笔记

129 阅读4分钟

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

第四天

性能优化及自动内存管理

性能优化的层面
业务代码
SDK
基础库
语言运行时
OS

e841c172-3e2e-4b6c-bdd9-d3b5009e6fd3

  • 在保证接口稳定的前提下改进具体实现
  • 测试用例:覆盖尽可能多的场景
  • 文档:做了什么,没做什么,能达到怎样的效果
  • 可观测:必要的日志输出

自动内存管理

  • 动态内存:程序在运行时根据需求动态分配的内存

  • 自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存

    • 避免手动内存管理,专注于实现业务逻辑
    • 保证内存使用的正确性和安全性
  • 三个任务:

    • 为新对象分配空间
    • 找到存活对象
    • 回收死亡对象的内存空间
  • Mutator: 业务线程,分配新对象,修改对象指向关系
  • Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
  • Serial GC:该GC算法只有一个collector
  • Parallel GC:并行GC,支持多个collectors同时回收的GC算法
  • Concurrent GC:并发GC,mutators和collectors可以同时执行

image-20230119182341875

这里的pause就是 stop the world

评价GC算法

  • 暂停时间:(Stop the world)业务是否感知
  • 内存开销

追踪垃圾回收

就是可达性分析:

三个步骤:

  • 标记根对象(静态变量、全局变量、常量、线程栈等)
  • 标记:找到可达对象
  • 清理:所有不可达对象

    • 标记 - 复制
    • 标记 - 清除
    • 标记 - 整理

分代GC

目的:对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销

不同年龄的对象处于heap的不同区域

  • 年轻代(新生代)

    • 常规的对象分配
    • 由于存活对象很少,可以采用 标记 - 复制算法
    • GC吞吐率高(所用时间少)
  • 老年代

    • 对象趋向于一直活着,反复复制开销较大
    • 可以采用 标记 - 清除(不如标记整理)

引用计数

下载

  • 优点:

    • 内存管理的操作被平摊到程序执行过程中
    • 内存管理不需要了解 runtime 的实现细节
  • 缺点:

    • 维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
    • 无法回收环形数据结构 ------ 可以使用弱引用来解决

    1d734293-5c3b-4d42-8ad6-1087e86c6a3b

    • 内存开销:每个对象都引入的额外内存空间存储引用数目
    • 回收内存时依然可能引发暂停

Go内存管理及优化

分块

  • 目标:为对象在heap上分配内存
  • 提前将内存分块
  • 调用系统调用mmap()向OS申请一大块内存,例如4MB
  • 先将内存划分成大块,例如8KB,称作mspan
  • 再将大块继续划分成特定大小的小块,用于对象分配
  • noscan mspan:分配不包含指针的对象 ---- GC不需要扫描
  • scan mspan:分配包含指针的对象 ---- GC需要扫描

75a9b261-8bb9-4a51-80a9-7cddaccb62f7

缓存

  • TCMalloc: thread caching

Go内存管理优化

  • 对象分配是非常高频的操作:每秒分配GB级别的内存

  • 小对象占比高

  • Go内存分配比较耗时

    • 分配路径长 g - > m - > p - > mcache - > mspan - > memory block - > return pointer
    • pprof:对象分配到

编译器和静态分析

305b62d2-8ec8-4167-905e-e66d7470f1be

静态分析

  • 控制流:程序执行的流程
  • 数据流:数据在控制流上的传递
  • 通过分析控制流和数据流,我们可以知道更多关于程序的性质

image-20230119190714887

  • 过程内分析:仅在函数内部进行分析
  • 过程间分析:考虑过程调用时参数传递和返回值的数据流和控制流

Go编译器优化

字节内部自研编译器优化方法

  • Beast mode

    • 函数内联

      • 内联:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

      • 优点

        • 消除函数调用开销,例如传递参数、保存寄存器等
        • 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
      • 缺点

        • 函数体变大,instruction cache 不友好
        • 编译生成的Go镜像变大
      • 函数内联在大多数情况下是正向优化

    • 逃逸分析

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

      • 大致思路:

        • 从对象分配处出发,沿着控制流,观察对象的数据流

        • 若发现指针p在当前作用域s:

          • 作为参数传递给其他函数
          • 传递给全局变量
          • 传递给其他的goroutine
          • 传递给已逃逸的指针指向的对象
        • 则指针p指向的对象逃逸出s,反之则没有逃逸出s

      • Beast mode:函数内联拓展了函数边界,更多对象不逃逸

      • 优化:未逃逸的对象可以在栈上分配

        • 对象在栈上分配和回收很快:移动 sp
        • 减少在heap上的分配,降低GC负担