这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天
高性能Go语言发行版优化与落地性能
优化的层面:业务代码 -> SDK -> 基础库 -> 语言运行时 -> OS
-
业务层优化
- 针对特定场景,具体问题,具体分析
- 容易获得较大性能收益
-
语言运行时优化
- 解决更通用的性能问题
- 考虑更多场景
- Tradeoffs
-
数据驱动
- 自动化性能分析工具 --- pprof
- 依靠数据而非猜测
- 首先优化最大瓶颈
自动内存管理
GC(Garbage collector)
自动内存管理也被称为垃圾回收,指的是由程序语言的运行时系统管理动态内存(malloc)。 使用自动内存管理可以:
-
避免手动内存管理,专注于实现业务逻辑
-
保证内存使用的正确性和安全性,在内存分配中有两个常见的问题:
- double-free problem:多次回收同一块内存
- use-after-free problem:在回收之后又使用该内存
自动内存管理的三个任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
自动内存管理 - 相关概念
-
Mutator:业务线程,分配新对象,修改对象指向关系
-
Collector:GC线程,找到存活对象,回收死亡对象的内存空间
-
Serial GC:只有一个 collector
-
Parallel GC:支持多个 collectors 同时回收的 GC 算法
-
Concurrent GC:mutators(s) 和 collector(s) 可以同时执行
- Collectors 必须感知对象指向关系的改变!
如何评价 GC 算法
- 安全性(Safety):不能回收存活的对象,这时基本要求
- 吞吐率(Throughout):1 - ,也就是花在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。
-
内存开销:每个对象都引入额外的存储空间来存储引用数目。
-
回收内存时依然可能引发暂停。