Skip to content

内存管理和性能优化

芯笑

前言:

本文主要介绍了自动内存管理中的常见GC算法和思想、Go的内存分配及字节的优化策略、编译器及其优化方法。

分析问题的方法与解决问题的思路,不仅适用于Go语言,其它语言优化也同样适用。

相关术语:

自动内存管理

Go 内存管理及优化

编译器和静态分析

Go 编译器优化

自动内存管理(GC)

背景:

GC 算法:

Serial GC:

Serial GC ,是新生代的垃圾回收器, Serial 体现在其收集工作是单线程的,并且在垃圾收集过程中,其他线程阻塞,进入 Stop Thre World 状态。新生代使用的 Serial 垃圾回收器,是基于复制算法的。

Parallel GC:

并行垃圾收集器:在young generation使用mark-copy,在Old Generation使用mark-sweep-compact;且在Young Generation和Old Generation 都会stop-the-world;收集器都使用多线程进行标记-复制和标记-压缩。

Concurrenty GC:

可以同时执行mutator和collector。

GC技术:

追踪垃圾回收:

追踪垃圾回收一般可分为三个步骤:

清理所有不可达对象清理策略,常见的有以下三种:

Copying GC:将对像复制到另外的内存空间

Mark-sweep GC:将死亡对象的内存标记为可分配,使用free list管理空闲内存

Mark-compact GC:移动并整理存活内存

分代GC(Generational GC):

我们看到有上述三种内存清理方法,那么我们应该如何去选择呢。这时就提出了一种思想:根据对象的生命周期,使用不同的标记和清理策略。

分代GC是一种常见的内存管理方式,基于这样一个假设:大部分新分配的对象存活周期较短,在分配后的第一轮GC中就会被回收掉。. 如果这个假设成立,那么GC期间只去扫描和清扫新分配的对象就可以清扫掉大部分需要回收的对象,这样就可以节省GC的时间。

分代GC认为,每个对象都有年龄(经历过的GC次数),不同年龄的对象处于heap的不同区域,这样做的目的可以针对年轻和老年对象,制定不同的GC策略,降低整体的内存管理开销。

引用计数:

引用计数 是计算机 编程语言 中的一种 内存管理 技术 ,是指将资源(可以是 对象 、 内存 或 磁盘 空间等等)的被 引用 次数保存起来,当被引用次数变为零时就将其释放的过程。

Go内存管理及优化:

Go内存分配:

分块:

为对象在heap上分配内存,根据对象的大小,选择最合适的快返回

分配步骤:

缓存:

通过上图可以看出,分配的路径长为:g—>m—>p—>mcache—>mspan—>memory block—>return pointer。对象的分配是非常高频的操作(GB/s),同时也非常耗时,从而导致cpu的消耗较高。

字节优化策略(Balanced GC):

Balanced GC的本质是一个对象,将多个小对象的分配合并一次达到对象的分配。同时就出现了弊端,此方式分配会导致内存被延迟释放( 在一个1K的GAB中只有一个8KB的使用,但此时这个空间也会被标记为活的)。此问题借鉴copying GC的算法解决,将存活的对象复制到另外的GAB中,然后释放原来的GAB。

个人思考: 当GAB总大小超过一定阈值时, 将GAB中存活的对象复制到另外分配的GAB中. 这里不太明白, GAB大小是否是固定的? 总大小超过阈值说明在使用中, 为什么还要拷贝? 复制到另外分配的GAB中, 新分配的GAB是否还是当前g独占, 一个g可以有多个GAB? 仔细想想这个GAB也没有那么简单。难,太难了。

编译器和静态分析:

编译器结结构:

编译器是系统软件,用于识别符合语法和非法程序,生成正确且高效的代码。

静态分析:

不执行程序代码,推导程序的行为,分析程序的性质。通过分析控制流和数据流,我们可以知道更多关于程序的性质,并根据这些性质优化代码。

过程内分析和过程间分析:

过程内分析:仅在函数内部进行数据流和控制流的分析

不考虑任何的过程/函数间调用的分析算法。 总的来说,这是最基础也是最简单的一类程序分析算法, 但是其实还是比较有用, 特别是在二进制分析的实际应用中这还算是比较常用的。

过程间分析:考虑函数调用时参数传递和返回值的数据流和控制流

动机1:在数据流分析中,很多转换函数的效果可以互相抵消,但我们还是要针对每一个进行计算。 动机2:程序分析中大量代码是库代码,往往分析一个很小的程序就要分析大量库代码。

Go编译器优化:

函数内联(Inlning):

将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

虽然有缺点,但是内联对于性能的影响还是很大的,以下代码可以使用micro-benchmark验证。

func BenchmarkInline(b *testing.B) {
	x := genIntegers()
	y := genIntegers()
	for i := 0; i < b.N; i++ {
		addInline(x, y)
	}
}
func addInline(x, y int) int {
	return x + y
}
func BenchmarkInlineDisabled(b *testing.B) {
 x := genIntegers()
 y := genIntegers()
for i := 0; i < b.N; i++ {
  addNoInline(x, y)
}
}
//go:noinline,强制要求不Inline
func addNoInline(x, y int) int {
  return x + y
}

Beast Mode:

函数内联受到的限制较多,因为Go的语言特性(defer),限制了函数的内联。

使用Beast Mode调整函数内联的策略,使更多函数被内联,降低了函数调用的开销,增加了其它优化机会(逃逸分析)

逃逸分析:

分析代码中指针的动态作用域(指针在何处可以被访问)

大致思路:

引用:

过程间分析

Java 常见的垃圾回收器-阿里云开发者社区 (aliyun.com)

分享
上一篇
Java自定义排序
下一篇
高质量编程-性能调优