自动内存管理和Go编译器优化 | 青训营笔记

73 阅读13分钟

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

1. 本堂课重点内容:

  1. 自动内存管理
  2. Go内存管理和优化
  3. 编译器和静态分析
  4. Go编译器优化

2. 详细知识点介绍

2.1. 自动内存管理

  1. 动态内存: 动态内存是程序运行时分配的内存,由程序员在运行时进行申请和释放。与静态内存相反,静态内存是在编译时分配的。

  2. 自动内存管理(垃圾回收): 由程序语言的运行时系统管理动态内存,负责自动释放不再使用的内存,以避免内存泄漏。

  3. 自动内存管理的三个任务: 垃圾收集,内存分配和内存碎片整理。

  4. 自动内存管理一些专业名词的相关概念:

    • Mutator: 是程序的代码部分,它可能会创建或销毁对象。
    • Collector: 是负责释放不再使用的内存的部分。
    • Serial GC: 是一种单线程垃圾回收器,在单独的线程中运行,不会影响到应用程序的性能。
    • Parallel GC: 是一种多线程垃圾回收器,使用多个线程同时进行垃圾回收,可以提高垃圾回收的效率。
    • Concurrent GC: 是一种并发垃圾回收器,能够在应用程序运行时进行垃圾回收,可以减少垃圾回收对应用程序性能的影响。
  5. GC算法原理(简单介绍): GC算法主要有两种,分别是标记-清除算法和复制算法。标记-清除算法首先标记出所有还在使用的对象,然后清除所有未被标记的对象,这样就可以释放掉不再使用的内存。而复制算法则是将内存分为两块,当其中一块内存用完后,将还存活的对象复制到另一块内存中,然后清除掉原来的内存。

  6. 评价GC算法:

    • 安全性: 垃圾回收器是否能保证不会回收仍然被使用的对象。
    • 吞吐率: 垃圾回收器对应用程序的性能影响,以及它能释放多少内存。
    • 暂停时间: 垃圾回收过程中,应用程序需要暂停多长时间。
    • 内存开销: 垃圾回收器需要多少额外的内存来进行工作。
  7. 追踪垃圾回收:

    • 回收条件: 垃圾回收器会在满足一定条件时进行垃圾回收,比如内存使用达到阈值或者程序运行一段时间。
    • 标记根对象: 垃圾回收器会从根对象开始遍历,标记所有可达的对象。
    • 标记: 垃圾回收器会标记所有可达的对象,将其设置为存活状态。
    • 清理: 垃圾回收器会清除所有未被标记的对象,释放内存。
    • 根据对象的生命周期,使用不同的标记和清理策略: 垃圾回收器可能会根据对象的生命周期来使用不同的标记和清理策略,比如分代GC会将对象分为年轻代和老年代,并采用不同的回收策略。
  8. 分代GC:

    • 分代假说: 分代假说认为对象的存活周期会分为年轻对象和老年对象。
    • Intuition: 分代GC的目的在于根据对象的存活周期来调整回收策略。
    • 每个对象都有年龄: 分代GC会将每个对象的存活时间称为对象的年龄。
    • 目的: 分代GC的目的在于根据对象的年龄来进行不同的回收策略。
    • 不同年龄的对象处于heap的不同区域: 分代GC会将不同年龄的对象放在不同的内存区域中,例如将新创建的对象放在年轻代中,将长时间存活的对象放在老年代中。
    • 年轻代: 是存活时间较短的对象,回收频率较高。
    • 老年代: 是存活时间较长的对象,回收频率较低。
  9. 引用计数:

    • 每个对象都有一个与之关联的引用数目: 引用计数算法会为每个对象维护一个引用计数器,表示有多少个对象引用了该对象。
    • 对象存活的条件: 对象存活的条件是引用计数大于0。
    • 优点: 引用计数算法简单易实现,可以及时回收对象。
    • 缺点: 引用计数算法无法回收循环引用的对象,会导致内存泄露。

2.2. Go内存管理及优化

  1. Go内存分配-分块:

    • 目标: 在Go中,内存分配是通过将内存分块来实现的。这样可以提高内存分配的效率,减少内存碎片。
    • 提前将内存分块: Go会提前将内存分块,将内存划分为多个大小相同的块。这样可以避免内存碎片问题。
    • 对象分配: 当程序需要分配对象时,Go会从提前分配的内存块中分配空间给对象。
  2. 缓存:

    • TCMalloc: Go使用了Google开发的TCMalloc作为内存分配器。它是一种高效的内存分配器,在保证内存分配效率的同时,也能有效减少内存碎片。
    • 每个P用来包含一个MCache用于快速分配: Go采用了多核并行技术,每个P都会有一个MCache用来快速分配对象。
    • MCache管理一组MSpan: MCache中包含多个MSpan,用于管理不同大小的内存块。
    • 当MCache中的MSpan分配完毕,向MCentral申请带有未分配块的MSpan: 当MCache中的MSpan的内存块被分配完毕,会向MCentral申请带有未分配块的MSpan。
    • 当MSpan中没有分配的对象,MSpan会被缓存在MCentral中,而不是立刻释放并归还给OS: 为了避免频繁申请内存块,当MSpan中的内存块没有被分配完毕,会将其缓存在MCentral中,而不是立刻归还给OS。
  3. 对象分配时非常高频的操作: 在Go中,对象分配是非常高频的操作,因此对内存分配器的性能有着较高的要求。

  4. 小对象占比较高: Go中,小对象的占比较高,因此内存分配器需要能够高效地分配小对象.

  5. Go内存分配比较耗时: 由于Go使用了高效的内存分配器和多核并行技术,在保证内存分配效率的同时,内存分配依然是一个耗时的操作。但是,go的gc算法也是十分优秀的,也是在这方面做了很多优化的.

    • 程序设计上,尽量减少内存分配次数,重复利用对象
    • 使用sync.Pool缓存小对象
    • 调整参数,如GOGC,GOMAXPROCS等
    • 使用pprof工具分析内存情况
    • 使用第三方工具来优化内存分配,如jemalloc

2.3. 编译器和静态分析

2.3.1 编译器的结构

image.png

  1. 重要的系统软件

    • 识别符合语法和非法的程序
    • 生成正确且高效的代码
  2. 分析部分(前端front end)

    • 词法分析,生成词素(lexeme)
    • 语法分析,生成语法树
    • 语义分析,收集类型信息,
    • 进行语义检查
    • 中间代码生成,生成intermediate representation(IR)
  3. 综合部分(后端back end)

    • 代码优化
    • 代码生成

2.3.2 静态分析

  1. 静态分析概念: 静态分析是指在程序未运行的情况下,对程序代码进行分析,了解程序的性质和行为。
  2. 控制流概念: 控制流分析是指对程序中的控制流进行分析,了解程序的执行顺序和控制结构。
  3. 数据流概念: 数据流分析是指对程序中数据的流动进行分析,了解数据如何在程序中传递和使用。
  4. 通过分析控制流和数据流,可以知道更多关于程序的性质: 通过对程序的控制流和数据流进行分析,可以了解程序的执行顺序,数据传递和使用情况,从而更好地理解程序的行为和性质。
  5. 根据这些性质优化代码: 通过对程序性质的了解,可以找到程序中的瓶颈和不足之处,并采取相应的优化措施来提高程序的性能和效率。

2.3.3 过程内分析和过程间分析

  1. 过程内分析: 过程内分析是指对单个函数或者程序块进行分析, 了解它的控制流和数据流.
  2. 过程间分析: 过程间分析是指对整个程序进行分析, 了解整个程序的控制流和数据流, 以及各个函数之间的调用关系.
  3. 为什么过程间分析十分重要: 过程间分析能够更好地了解整个程序的整体行为和性质, 更容易发现程序中的瓶颈和性能问题, 从而更好地进行性能优化. 另外, 过程间分析还能帮助我们了解程序的模块化结构和代码的可维护性.

2.4. Go编译器优化

  1. 为什么做编译器优化: 编译器优化是在编译阶段对程序代码进行优化, 使得程序能够更高效地运行. 优化编译器能够帮助我们提高程序性能, 提高程序的效率.
  2. 编译器优化现状: 编译器优化是一种技术性强, 研究难度高的优化方式, 目前还没有一种通用的编译器优化算法.
    • 采用的优化少
    • 编译时间短,没有进行复杂的代码分析和优化
  3. 编译优化的思路: 编译器优化的核心思路是通过对程序代码的分析, 找到程序的瓶颈, 并采取相应的优化措施来提高程序的性能.
    • 场景:面向后端长期执行任务
    • traeoff:用编译时间换取更高效的机器码
  4. Beast mode: Beast mode是一种编译器优化策略, 通过调整函数内联的策略, 使更多的函数被内联, 来提高程序的性能.

2.4.1 函数内联

  1. 函数内联: 函数内联是在编译时将函数的实际代码展开到调用处, 以减少函数调用的开销.
  2. 优点:函数内联能够提高程序性能, 优化程序运行效率, 减少函数调用的开销.
  3. 函数内联能多大程度影响性能: 函数内联能够在很大程度上提高程序性能, 但具体影响程度取决于程序中函数调用的频率和函数体大小.
  4. 缺点: 函数内联会增加程序的代码大小, 并且在一些情况下可能会导致程序运行变慢.
  5. 内联策略: 编译器会根据函数的调用频率和函数体大小来决定是否进行内联.
  6. 函数内联在大多数情况下是正向优化: 函数内联能够提高程序性能, 但是在某些情况下可能会导致程序变慢, 所以在使用函数内联优化时需要谨慎.

2.4. 2 BeastMode

  1. Go函数内联受到的限制较多

    • 语言特性,例如interface,defer等,限制了函数内联
    • 内联策略非常保守
  2. Beast mode:调整函数内联的策略,使更多函数被内联

    • 降低函数调用开销
    • 增加了其它优化机会:逃逸分析
  3. 开销

    • Go镜像增加 ~10%
    • 编译时间增加
  4. 逃逸分析

    • 概念: 逃逸分析是指分析程序中对象的生存周期,判断对象是否可能在函数调用结束后仍然存在.
    • 大致思路: 逃逸分析通过分析程序的控制流和数据流来确定对象是否可能逃逸出函数.
    • Beast mode: Beast mode通过对逃逸分析的优化来提高程序性能.
    • 优化: 通过逃逸分析可以确定哪些对象可能逃逸出函数, 从而采取相应的优化措施,如将对象分配到栈上,而不是堆上。这样可以避免不必要的垃圾回收开销。
    • 具体实现: 在编译时, 编译器会对程序进行逃逸分析, 判断对象是否逃逸出函数。如果对象没有逃逸出函数,则将其分配到栈上。
    • Beast mode和逃逸分析结合使用: Beast mode会在进行逃逸分析的基础上,进一步优化程序,使得更多的对象能够分配到栈上。这样可以提高程序性能,并减少垃圾回收带来的开销。

3. 实践例子举例

3.1 Balanced GC

  • Go语言的垃圾收集器采用了一种名为"Balanced GC"的算法。这种算法通过控制垃圾收集周期的长短来平衡吞吐量和停顿时间。

  • GAB(Generational And Adaptive)是Go语言垃圾收集器中用于改进Balanced GC算法的一种技术。

  • GAB通过将堆内存分成若干代(generation)来提高垃圾回收效率。每一代都有自己的垃圾回收策略和频率。新分配的对象都在新生代中,而老对象都在老生代中。由于新生代对象的存活率较低,因此新生代的垃圾回收频率更高,而老生代的垃圾回收频率更低。这样就可以大大提高垃圾回收效率。

  • GAB和Balanced GC的本质区别在于它通过分代来提高垃圾回收效率。而Balanced GC算法则是通过调整垃圾回收频率来平衡吞吐量和停顿时间。

  • GAB技术在解决Balanced GC算法存在的问题,例如:

    • Balanced GC算法无法有效地回收长寿对象,因为它们只会在极少数垃圾回收周期中被扫描到。
    • Balanced GC算法无法有效地回收短寿对象,因为它们会在较长的垃圾回收周期中被扫描到。
  • GAB算法通过在新生代中进行频繁的垃圾回收,来减少长寿对象的影响,并在老生代中进行较少的垃圾回收,来减少短寿对

3.2 Beast Mode

  • 在Go语言中,函数内联是指将函数体的代码直接展开到调用处的一种优化技术。这样可以减少函数调用的开销,提高程序的性能。

  • 在 "Beast Mode" 下,编译器会尽可能多地进行函数内联优化。这样可以提高程序的运行速度,但可能会增加代码体积。

  • 在 "Beast Mode" 下运行的系统中,编译器可能会忽略一些优化选项,例如函数内联的代码体积限制, 这样会使程序更快,但是可能会导致程序更大。

  • 另外,在 "Beast Mode" 下,编译器也可能会忽略一些优化选项,例如跳过不经常使用的代码路径的优化,这样可以提高程序运行速度,但可能会导致程序的代码体积增大。

  • 总之,在 "Beast Mode" 下,编译器会尽可能多地进行函数内联优化,以提高程序的性能,但可能会导致程序的代码体积增大。