青训营第三期 | 高性能优化

131 阅读5分钟

为什么要做性能优化

  • 提升用户体验
  • 资源高效利用 --->降低成本,提高效率

自动内存管理

动态内存管理

  • 根据需求动态分配内存:malloc()

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

  • 避免手动内存管理,专注于实现业务逻辑
  • 保证内存使用的正确性安全性: double-free problem, use-after-free problem

垃圾回收的三个任务

  • 为新对象分配空间
  • 找到存活对象
  • 回收死亡对象的内存空间

基础概念

  • Mutator: 业务线程,分配新对象,修改对象指向关系

  • Collector: GC 线程,找到存活对象,回收死亡对象的内存空间

  • Serial GC: 只有一个 collector

image.png - 会引入暂停:STW - 只有一个垃圾回收

  • Parallel GC: 并行 GC,支持多个 collectors 同时回收的 GC 算法

image.png

  • Concurrent GC: 并发 GC,支持 mutator(s) 和 collector(s) 同时执行的 GC 算法

image.png

  • 必须感知对象指向关系的改变! (也就是新对象进来,需要被活对象指向,不然会被Collector清除)

    ![image-20220511163003837]()
    

image.png

  • GC算法的评估标准:

    • 安全性:存活的对象不能被回收
    • 吞吐率:1 - GC时间/总时间 ➡️ 花在GC上的时间越少越好
    • 暂停时间:STW
    • 内存开销:GC所带来的开销

image.png

垃圾回收常见算法:

  • Tracing garbage collection: 追踪垃圾回收

image.png

  • 标记根对象:静态变量、全局变量、常量、线程栈等

  • 标记:找到可达对象(从根对象出发找到所有可达对象)

  • 清除:清理所有不可达对象(清理方式有多种)

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

    image.png

    • Mark-sweep GC:将死亡对象的内存标记为”可分配“

    image.png

    • Mark-compact GC: 将存活对象复制到同一块内存区域的开头

    image.png

    • 根据对象的生命周期,来选择不同的标记和清理策略
例子:
  • 分代GC(Generational GC)

image.png

引用计数

image.png

  • 每个对象都有一个与之关联的引用数目

  • 对象存活的条件:当且仅当引用数大于 0

  • 优点

    • 内存管理的操作被平摊到程序运行中:指针传递的过程中进行引用计数的增减
    • 不需要了解 runtime 的细节:因为不需要标记 GC roots,因此不需要知道哪里是全局变量、线程栈等
  • 缺点

    • 开销大,因为对象可能会被多线程访问,对引用计数的修改需要原子操作保证原子性和可见性

    • 无法回收环形数据结构

    • 每个对象都引入额外存储空间存储引用计数

    • 虽然引用计数的操作被平摊到程序运行过程中,但是回收大的数据结构依然可能引发暂停(对于执行的程序太多,可能需要暂停来回收

image.png

  • 实际例子:

Go内存管理和优化

  • TCMalloc: TC is short for thread caching

image.png - 目标:为对象在 heap 上分配内存

  • 提前分配内存分块:

    • 系统调用mmap()去向OS申请一块大内存,例如4MB
    • 将内存分成多个块,例如8KB,称为mspan
    • 将大块再切成成多个特定小块,用于对象分配
    • noscan mspan:分配不包含指针的对象(GC不需要扫描 )
    • scan msapn:分配包含指针的对象(GC需要扫描)
  • 对象分配:根据对象的大小选择最合适的块返回

  • 内存缓存:

image.png

小对象分配最占用时间,所以优化是优化对象分配这个过程

  • 字节的Balance GC

    • 记录大小是类似于压缩链表,前面有字段存储使用的长度
    • mcache存储基本所有类型的mspans

编译器和静态分析

img

image.png

主要介绍后端方面的优化

  • 静态分析:

    • 静态分析:不执行代码,推导程序的行为,分析程序的性质。(先静态去分析,而避免很多代码的加载)
    • 控制流:程序的执行流程(CFG来将其编排起来)
    • 数据流:数据在控制流上的传递
  • 通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties) ,这些事实可以帮助我们做编译优化

  • Intra-procedural analysis(过程内): 函数内分析:在函数内进行控制流和数据流的分析

  • Inter-procedural analysis(过程间): 函数间分析:除了函数内的分析,还需要考虑跨函数的数据流和控制流,例如参数传递,函数返回值等

image.png

Go编译器优化:

目的

  • 用户无感知,重新编译即可获得性能收益
  • 通用的优化手段

现状

  • 采用的优化较少
  • 追求编译时间短,因此没有进行复杂的代码分析和优化

函数内联:

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

  • 优点

    • 消除调用开销
    • 将过程间分析的问题转换为过程内分析,帮助其他分析
  • 缺点

    • 函数体变大(递归的时候导致函数特别长)
    • 编译生成的 Go 镜像文件变大

逃逸分析:

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

  • 大致思路

    • 从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s (当前之外的作用域也能访问到) :

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

  • 优化:未逃逸出当前函数的指针指向的对象可以在栈上分配

    • 对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收;

    • 减少在堆上分配对象,降低 GC 负担。

image.png 总结:

image.png

\