高性能Go语言 | 青训营笔记

87 阅读4分钟

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


本堂课主要内容是Go语言的自动内存管理、内存分配以及编译器优化,下面主要是我个人听课时的一些笔记。

个人笔记

Go自动内存管理

  • 自动内存管理是对于动态内存而言的

    如程序在运行时根据需求动态分配的内存:malloc()

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

    • 避免手动内存管理,专注于实现业务逻辑

    • 保证内存使用的正确性和安全性:double-free problem, use-after free problem

  • 三个任务:

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

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

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

    Serial GC: 只有一个collector

    Parallel GC: 支持多个collectors 同时回收的GC算法

    Concurrent GC: mutator(s)和collector(s)可以同时执行

    • concurrent GC的难点在于,collectors必须要感知到对象指向关系的改变

      GC过程中新添加的对象,也需要被标记

    • 评价GC算法
      • 安全性(Safety): 不能回收存活的对象 基本要求
      • 吞吐率(Throughput): 1GC时间程序执行总时间1-\frac{GC时间}{程序执行总时间}
      • 暂停时间(Pause time): stop the world (STW) 业务是否感知
      • 内存开销(Space overhead) GC元数据开销
    • 追踪垃圾回收(Tracing garbage collection)
    • 引用计数(Reference counting)
  • 追踪垃圾回收(Tracing garbage collection)

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

    • 标记跟对象
    • 标记:找到可达对象
    • 清理:所有不可达对象

    清理不可达对象时会有三种策略

    • Copying GC

      将对象复制到另外的内存空间

    • Mark-Sweep GC

      使用free-list管理空闲内存

    • Mark-Compact(标记-压缩) GC

      原地整理对象

  • 分代GC(Generational GC)场景

    分代假说:most objects die young

    年轻代采用copying GC,因为其存活时间短,所以使用copying GC不会特别耗时

    老年代采用Mark-sweep GC,对象存活时间长,反复复制开销较大

    若发现碎片较多,可以采取一次Compact(压缩)

  • 引用计数(Reference counting)

    计算该对象被多少个指针“引用”,即被多少个指针指向

    优点:

    • 内存管理的操作被平摊到程序执行过程中

    • 内存管理不需要了解runtime的实现细节,如C++的智能指针(smart pointer)

      追踪垃圾回收(Tracing garbage collection)需要和runtime耦合的更紧密

    缺点:

    • 维护引用计数的开销较大: 通过原子操作保证对引用计数操作的原子性可见性

    • 无法回收环形数据结构

      通过weak reference解决这个问题

    • 内存开销: 每个对象都引入的额外内存空间存储引用数目

    • 回收内存时依然可能引发暂停,如源头节点释放后,会引起很多内存的回收,这个过程中仍然可能引发暂停

Go内存分配

  • 分块:为对象在heap上分配内存

    提前将内存分块

    根据对象的大小,选择最合适的块返回

  • 缓存

  • 优化原因:

    • 分配路径过长
    • 小对象过多

    优化案例:Balanced GC(指针碰撞风格对小对象进行分配)

Go编译器优化

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

  • Beast Mode:调整函数内联的策略,使更多函数被内联

    • 降低函数调用的开销
    • 增加了其他优化的机会,如逃逸分析
  • 逃逸分析:分析代码中指针的动态作用域,即指针在何处可以被访问

    函数内联可以减少逃逸对象的数量

    未逃逸的对象可以在栈上分配,对象在栈上的分配和回收很快

参考

  • 字节内部课—Go 语言内存管理详解