性能优化是为了提高软件的性能,减少不必要的开销。这可以有效提升用户体验与降低成本,提高资源利用率。
一、自动内存管理
程序在运行时会根据需要动态分配内存(如malloc)。自动内存管理就是在程序运行时管理动态内存的机制。 主要作用:
- 避免手动内存管理,开发者只需要关注于业务逻辑(更符合“Keep it simple, stupid”原则)
- 保证内存使用的正确性和安全性,可以有效防止在动态内存管理过程中出现的问题(如double-free、uaf等) 自动内存管理主要实现了三个任务:
- 为新对象分配空间
- 确定存活对象
- 回收已经不使用的对象的内存
1.相关概念
- Mutator:业务线程
- Collector:GC线程,可以实现动态内存管理
- Serial GC:只有一个Collector,多个Mutator共用。
- Parallel GC:支持多个Collector。
- Concurrent GC:Mutator和GC同时执行。 Concurrent GC需要Collectors感知对象的指向关系发生变化,因为在Mutator和GC线程的并发执行过程中,不可避免的会发生对象之间的引用、指向关系的改变(可能比较类似于访问临界区)。比如一个存活对象引用A对象时,GC会将二者都标记为存活对象,如果同时Mutator修改该存活对象使得它引用B对象,这就是对象的指向关系发生变化,需要GC去处理。
2.相关指标
- 安全性(正确性):不能回收存活对象。(这是GC的正确性基本要求)
- 吞吐量:1-(GC时间)/(程序执行总时间)
- 暂停时间
- 内存开销
3.追踪GC
追踪GC本质上是将对象之间的引用关系抽象为一个图结构。 追踪GC中,全局变量(Global Var)以及栈(Stack)等区域的指针所指向的堆区(Heap)的对象视为根对象,之后GC会根据对象之间的引用关系不断遍历,从而将所有对象划分为两个集合——可达对象与不可达对象。不可达对象将会被视为未使用对象而被回收。 GC中有三种回收策略:
- Copying GC:将存活对象复制到另外的内存空间。因为复制需要额外的开销,所以通常在需要复制的对象较少时或必要时使用。
- Mark-sweep GC:将未使用的对象的内存空间标记为可分配的,使用一个free链表来管理可供分配的动态管理区域(ptmalloc的free策略)。
- Compact GC:原地整理对象,类似于Copying GC
4.分代GC
分代GC基于的假设是:很多对象分配出来以后很快就不再使用(如函数返回后,函数中定义的临时变量就不再使用)。分代GC是根据对象的存活周期,制定不同的策略。 主要的思路是:
- 将不同年龄的对象置于heap的不同区域
- 对不同年龄的对象,制定不同的GC策略,降低整体内存管理开销
- 对于年轻代(经历过GC次数少,生命周期较短)的对象,由于很多对象在分配后很快不再使用,存活对象少,使用copying GC,复制的对象少,开销较小,GC吞吐率高。
- 对于老年代,对象存活周期长,反复复制开销大,所以采用mark-sweep GC
5.引用计数
每个对象都有与之关联的对象的数目(即引用它的指针的数目)。当且仅当引用数大于0时对象存活(未必,可能有循环引用) 优点:
- 快:GC在程序执行过程的同时执行
- GC不需要了解具体runtime实现细节 缺点:
- 原子操作:本质来说,引用计数属于临界区,需要使用原子操作来保证其正确性,会导致维护引用计数开销增大。
- 无法回收循环引用的对象
- 内存开销:引入额外内存空间存储引用计数
二、Go内存管理及优化
1.分块
在mmap()申请一块大内存后,再分配为大块(如8KB),称为mspan,进一步将mspan按照特定大小划分为更小的块,并设定对象是否包含指针(可以决定是否需要GC)。分配时按照对象大小分配。
2.缓存
维护mcache,优先mcache上分配,不够时向mcentral申请新mspan
3.Balanced GC
设计原理:对象分配高频,且小对象占比较高 解决的问题:内存分配流程长,耗时。 思路:
- 每个goroutine分配一个大块内存(goroutine allocation buffer),用于noscan(无对象引用)的小对象分配
- 维护base、end、top指针
- 分配内存时只需移动top指针即可
- 设定阈值,当GAB大小超过阈值时,采用Copying GC将存活对象分配到其他GAB中,并释放源GAB。 本质思路就是:将多个小对象的分配合并为一个大对象。这样将频繁的减少了频繁分配小对象的开销。
三、编译器与静态分析
1.静态分析
在不执行代码的情况下推导程序行为,分析程序性质。 主要采用控制流(程序语句的执行流程)与数据流(数据在控制流上的传递) 编译器的代码优化主要是根据静态分析得出的性质对代码进行简化
2.过程内分析和过程间分析
- 过程内分析:仅分析函数内部
- 过程间分析:考虑多个函数间调用与返回的关系,需要同时考虑控制流与数据量,较复杂
3.优化思路
主要思路:用编译时间换运行时间(仅指产品上线)
- 函数内联:将被调用函数复制到主调函数中,从而将过程间分析转为过程内分析(问题转化)。也可以减少函数调用开销
- Beast Mode:调整Go语言内联策略,让更多函数内联。
- 逃逸分析:分析代码指针的作用域,未逃逸的对象可以在栈上分配。(Beast Mode可以让更多变量不逃逸)
总结:考虑了在底层对Go进行优化 在Go语言动态内存分配方向,主要是减少了频繁的小对象申请内存的开销,大大提升了性能。 在编译器优化方面,主要思路是减小编译器的静态分析的复杂度,从而生成更高质量的机器代码。