这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记
Go语言优化
优化的层级
- 提升系统处理能力,减少不必要的消耗
- 提升用户体验
- 对资源高效利用
- 业务层优化:需要结合业务场景进行具体分析,收益一般会很明显;
- 针对语言的运行时优化:需要解决一些通用的性能问题;
- 内存分配,编译优化;从数据中分析性能瓶颈,优先解决最大的性能瓶颈;
自动内存回收机制:
- Mutator 用来做内存分配修改的业务线程,给对象分配内存,修改指向关系;
- Collector Gc线程 用来自动回收不被使用的对象的内存空间,
- Serial GC 序列化Gc 收集内存使用一个collector 来处理
- Parallel GC 并行使用多个collector 来回收Gc
- Concurrent GC 类似生产者消费者的模式,分配回收线程一起执行 对于GC 算法效率的评价:
暂停时间是和一般算法评价有点不一样的指标,因为GC回收的时候大多会砸瓦鲁多,阻塞一段时间保证安全;
两种主流的内存回收算法
- 追踪垃圾回收,指针指向关系不可达的对象,从标记的根对象找到所有的可以达到的对象,标记保留,不可达的对象清理;重要的一点就是用来标记的根对象一般是静态变量,全局变量,常量,线程栈;
对于不可达对象的清理,对还存活的对象复制到另外的堆分区,或者在移动对象之后对象会进行整理,可能是内存空间的对其,挺高内存的利用效率;有点像按照存活时间划分的年代分区方式;将死亡也就是不会再使用的内存标记为可分配,此部分内存将会再次被分配出去用来创建对象;
- 引用计数方法,引用数量大于0 优点 内存管理成本被分摊到执行过程中; 内存管理和运行时细节无关,相似的是C++的智能指针;缺点开销大,需要使用原子操作来计数,无法回收环形引用;每个对象都需要维护一个引用计数的外部内存空间,回收内存也会引发处理暂停;
分代假设:
- 很多对象的存活时间很短,很快就会不再使用;
- 针对对象经历的GC 内存回收次数来采用不同的回收策略
- 堆中对不同年代分区的对象划分了不同的内存分区,可以提高内存回收的效率,因为基本上新生代基本上是复制存活对象后,全区回收;老年代只回收死亡对象;
- 引用计数的内存回收算法
Go的内存管理和优化;
go的处理是对内存做了预分块;会将申请的堆空间经过多次分块处理为对象分配使用的内存页? mspan 内存段分做两种:
- noscan 不存在指针的对象,不需要GC扫描
- scan mspan 包含指针的对象需要gc扫描 这里的扫描感觉更适合叫做递归标记
Go 内存分配 线程缓存 TCMalloc:GAB
优先将缓存里面的内存空间给分配出去,这种思想类似于快表,在mcentral 中的未分配的内存空间不会主动提交给系统,而是等缓存中内存不足的时候提交上去; 指针碰撞风格的内存分配:
if top+size <=end:
addr=top
top +=size
return addr
/ * 仔细看还是很像 return num++;*/
- 为每一个协程绑定一个1kb 内存叫做分配缓冲,可以提供对于小对象的分配效率;
- 将协程产生的多个对象合并为一个批量分配;但是批量内存管理的方式可能因为缓冲当中存在未死亡的对象导致整个缓冲释放被延迟到所有对象死亡的时候执行,也就是常见的内存泄露,可能导致内存效率下降;针对这种情况,当当前的协程缓冲内存被分配到一定比例的时候会把拷贝存活的对象到一个新的缓冲空间中,然后释放原先的空间,避免内存泄露
静态分析 不执行代码,来推导程序的行为, 控制流分析,分析程序执行的流程,控制流图 数据流,分析数据在控制流上的传递;
- 过程内分析,针对函数内部进行分析;
- 过程间分析,考虑函数调用是参数传递和返回值的数据流和控制流;
- 过程间分析需要考虑对象类型才能知道调用的是哪个函数
编译器优化:
- 函数内联;这里C++ 里面inline的内联关键字会建议编译器将声明的函数进行内联也就是声明成一个语句避免一个简单操作也需要栈上进出的开销和相关参数传递和寄存器开销;同时可以将函数嵌套分析转换为一个函数的内部分析;函数内联会提升编译器时长,但是运行时效率会提升。go的函数内联策略比较保守; 使用 Beast Mode 内联策略,能够提升函数被内联的几率;
- 内存逃逸分析;内存逃逸;在Java 中一些强引用的对象因为对象没有被自动释放,go 中指针传递给其他函数,传递给全局变量,传递给其他协程,传递给已经逃逸的指针指向的对象;则指针指向的对象已经逃逸;没有逃逸的对象会优先在栈上分配,栈上分配回收效率很高,只要一动栈顶指针就行,同时避免了GC开销;也就是使得小的对象尽量在栈上创建分配;使用一些小例子来验证优化带来的性能提升;