高性能Go语言发行版优化与落地性能 | 青训营笔记

98 阅读4分钟

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

高性能Go语言发行版优化与落地性能

优化的层面:业务代码 -> SDK -> 基础库 -> 语言运行时 -> OS

  • 业务层优化

    • 针对特定场景,具体问题,具体分析
    • 容易获得较大性能收益
  • 语言运行时优化

    • 解决更通用的性能问题
    • 考虑更多场景
    • Tradeoffs
  • 数据驱动

    • 自动化性能分析工具 --- pprof
    • 依靠数据而非猜测
    • 首先优化最大瓶颈

自动内存管理

GC(Garbage collector)

自动内存管理也被称为垃圾回收,指的是由程序语言的运行时系统管理动态内存(malloc)。 使用自动内存管理可以:

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

  • 保证内存使用的正确性和安全性,在内存分配中有两个常见的问题:

    • double-free problem:多次回收同一块内存
    • use-after-free problem:在回收之后又使用该内存

自动内存管理的三个任务:

  1. 为新对象分配空间
  2. 找到存活对象
  3. 回收死亡对象的内存空间

自动内存管理 - 相关概念

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

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

  • Serial GC:只有一个 collector

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

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

    • Collectors 必须感知对象指向关系的改变!

如何评价 GC 算法

  • 安全性(Safety):不能回收存活的对象,这时基本要求
  • 吞吐率(Throughout):1 - GC时间程序执行总时间\frac{GC时间}{程序执行总时间},也就是花在GC上的时间越少,吞吐量越高
  • 暂停时间(Pause Time):Stop The World(STW),这里需要注意控制时间长度,业务感知很重要
  • 内存开销(Space overhead):GC元数据开销,也就是GC所需要消耗的内存空间

追踪垃圾回收

被回收的对象是指针指向关系不可达的对象,因为不可达就代表没有被依赖。

垃圾回收的过程由以下几点组成:

  • 标记根对象

    根对象包括静态变量、全局变量、常量、线程栈等,很容易想到因为其从程序一开始运行就被整个程序依赖,因此根对象就是他们。

  • 标记:找到可达对象

    从根对象出发,找到所有可达的对象,也就是求指针指向关系的传递闭包

  • 清理:所有不可达对象

    该步骤有三种策略,如下所述:

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

    清理大的内存区域,把大的区域中存活对象复制到另外的内存空间,原来大的区域就可以清空了。

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

    在分配的时候,在free list中找一个对象分配,就可以了。

    • 移动并整理存活对象(Mark-compact GC

原地整理空间,这样之后再分配空间,就可以在第三个黑块后面分配。

根据对象的生命周期,需要使用不同的标记和清理策略。

分代GC

分代GC(Generational hypothesis):most objects die young,其认为很多对象在分配出来之后很快就不再使用了,而每个对象都有年龄,也就是经过GC的次数。

它的目的就是针对年轻和老年的对象,制定不同的GC策略,从而降低整体内存管理的开销。

还有重要的一点就是不同年龄的对象处于heap的不同区域。

  • 年轻代(Young generation)

对于年轻代,也就是常规的对象分配,由于其假设,存活对象很少,因此可以采用Copying GC,这种情况下,GC的吞吐率很高。

  • 老年代(Old generation)

对于老年代,其中的对象趋向于一直存活着,若反复复制,开销将会很大,因此适合采用Mark-sweep GC

引用计数

其特点是每个对象都有一个与之关联的引用数目,因此该对象存活的条件就是其引用数目大于0。别人引用它,它的引用计数+1。

优点

  • 内存管理的操作被平摊到程序的执行过程中
  • 内存管理不需要了解runtime的实现细节,C++的智能指针(smart pointer)就是类似的方法

缺点

  • 维护引用计数的开销很大,因为需要通过原子操作保证对引用计数操作的原子性和可见性

  • 无法回收环形数据结构,也就是weak reference。

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

  • 回收内存时依然可能引发暂停。