这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
课程主要内容是Go语言内存管理优化和编译器优化:
1. 自动内存管理
2. Go内存管理及优化
3. 编译器和静态分析
4. Go编译器优化
前言
接性能优化课程内容,性能优化能够带来更好的用户体验和资源的高效利用。可以分为两个层面,业务层优化(具体问题具体分析,较大性能收益)和语言运行时的优化(解决更通用的性能问题以及权衡)。这节课程侧重语言运行时的优化。
1.自动内存管理
主要任务是为新对象分配空间,找到存活对象,回收死亡对象的内存空间。
相关概念包括:Mutator:业务线程,分配新对象,修改对象指向关系;Collector: GC线程,找到存活对象,回收死亡对象的内存空间;Serial GC:只有一个collector;Parallel GC:支持多个collectors同时回收的GC算法;Concurrent GC: mutator(s)和collector(s)可以同时执行。
追踪垃圾回收
指针指向关系不可达的对象就能回收,过程:标记根对象(静态、全局 常量、线程栈),标记找到可达对象(根据指针指向关系传递的闭包),最后清理所有不可达对象(Copying GC, Mark-sweep GC, Mark-compact GC)。
分代GC
对年轻和年老的对象,制定不同GC策略来降低内存管理开销。让不同年龄对象处于heap不同区域。年轻代存活较少,采用Copying GC。老年代存活较多,采用Mark-sweep GC。
引用计数
对象都有与之关联的引用数目,对象存活条件,当且仅当引用数目大于0。程序执行过程就能进行内存管理。缺点是要通过原子操作保证引用计数操作原子性和可见性,无法回收环形数据结构,需要额外开销存储引用数目,回收可能引发暂停。
2.Go内存管理及优化
Go内存分配包括分块和缓存。内存分配非常高频,并且小对象占比高,分配耗时,因为分配路径非常长。
Balanced GC
字节Balanced GC,每个协程绑定一块1KB内存,即GAP。用于小对象分配,采用三个指针base top end进行分配,不需要和其他分配请求互斥,分配动作简单高效。
GAP对于Go内存管理是一个大对象,将多个小对象合并成一次大对象分配,这样会产生问题就是一个小对象存活,那整个GAP不会被回收。解决办法是采用新的GAP,将不同GAP存活的小对象采用Copying GC管理。
3.编译器和静态分析
编译器结构分为前端和后端,前端主要分析,后端综合。主要学习后端的优化。静态分析指不执行程序代码推导程序行为和分析程序性质,包括控制流和数据流。
过程内分析和过程间分析
过程内分析在过程内部分析,过程间要考虑过程调用的参数传递和返回值数据流和控制流,联合求解比较复杂。
4.Go编译器优化
编译器优化是用户无感知重新编译就能获得性能收益,并且通用的优化。采用的优化少,目前都追求编译时间短,没有进行复杂代码分析和优化。因此思路是面向后端长期执行任务,用编译时间换取高效机器码。字节Beast mode包括函数内联合逃逸分析还有其他。
函数内联
将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定这样可以消除函数调用开销,例如传递参数、保存寄存器等,将过程间分析转化为过程内分析,帮助其他优化,例逃逸分析。
缺点是函数体变大,编译生成的Go镜像变大。函数内联在大多数情况下是正向优化但还需要内联策略来调整,例如调用和被调函数的规模。
逃逸分析
分析代码中指针的动态作用域:指针在何处可以被访问。
大致思路是:从对象分配处出发,沿若控制流,观察对象的数据流;若发现指针p在当前作用域s作为参数传递给其他函数、传递给全局变量、传递给其他的goroutine、传递给已逃逸的指针指向的对象,则指针p指向的对象逃逸出s,反之则没有逃逸出s。
Beast mode:函数内联拓展了函数边界,更多对象不逃逸。优化:未逃逸的对象可以在栈上分配:对象在栈上分配和回收很快:移动sp;减少在heap上的分配,降低GC负担。
总结
学到了很多,包括自动内存管理优化以及编译器优化,主要就是在语言运行时的优化,字节对这两方面的优化思路也能够应用在其他语言上,是个很好的借鉴。