本文是记录2022年5月字节跳动青训营的课程笔记!!!
本文内容:
优化:
内存管理优化
编译器优化
背景:
自动内存管理和Go内存管理机制
编译器优化的基本问题和思路
实践:
字节跳动遇到的性能问题和优化方案
性能优化:
性能优化是什么:
提升软件能力,减少不必要的消耗,充分发掘计算机的能力
为什么要做性能优化:
用户体验:带来用户体验的提升
资源高效利用:降低成本、提高效率
1.自动内存管理
1.1背景及概念
动态内存:
程序在运行时根据需求动态分配的内存
自动内存管理:
由程序语言的运行时系统管理动态内存
优点:
避免手动内存管理,专注于实现业务逻辑
保证内存使用的正确性和安全性
三个任务:
为新对象分配空间
找到存活对象
回收死亡对象的内存空间
Mutator:业务线程,分配新对象,修改对象的指向关系
Collector:GC线程,找到存活对象,回收死亡对象的内存空间
Serial GC:只有一个GC
ParallelGC:支持多个collectors同时回收GC的算法
Concurrent GC:mutator和collector可以同时执行
注:Collectors必须感知对象指向关系的改变
评价GC算法:
安全性:不能回收存活对象 基本要求
吞吐率:1-GC时间/程序运行总时间 花在GC上的时间
暂停时间:STW 业务是否感知
内存开销:GC元数据开销
1.2追踪垃圾回收
对象被回收的条件:指针指向关系不可达的对象
标记根对象(静态变量、全局变量、常量、线程栈等)
标记:可达对象
求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
清理:所有不可达对象
将存活对象复制到另外的内存空间(Copying GC)
将死亡对象的内存标记为“可分配”(Mark-sweep GC)
移动并整理存活对象(Mark-compact GC)
根据对象的生命周期,使用不同的标记和清理策略
分代GC
跟Java JVM 类似
Java 虚拟机(JVM)学习笔记_芙蓉铁蛋的博客-CSDN博客
对年轻和老年代的对象,制定不同的GC策略,降低整体内存管理开销
不同年龄的对象处于heap的不同区域
年轻代:常规的对象分配,由于存活对象很少可以采用copying collection GC吞吐率高
老年代:对象趋向于一直活着,反复复制开销较大,可以采用mark-sweep collection
1.3引用计数
每一个对象都有一个与之关联的引用数目
对象存活条件:当且仅当引用数大于0
优点:内存管理的操作被平摊到程序执行过程中
内存管理不需要了解runtime的实现细节:C++智能指针
缺点:维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
无法回收环形数据结构
内存开销:每个对象都引入了额外的内存空间存储引用数目
回收内存时依然可能引发暂停
环形数据结构:
2.Go内存管理及优化
2.1Go内存分配
分块
目标:为对象在heap上分配内存
提前将内存分块
调用系统调用mmap()向OS申请一大块内存,例如4MB
先将内存划分为大块,例如8KB,成为mspan
再将大块继续分成特定大小的小块,用于对象分配
noscan mspan:分配不包含指针的对象—GC不需要扫描
scan mspan:分配包含指针的对象—GC需要扫描
对象分配:根据对象的大小,选择最合适的快返回
缓存
TCMalloc:thread caching
每个p包含一个mcache用于快速分配,用于绑定p上的g分配对象
mcache管理一组mspan
当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
当mspan中没有分配的对象,mspan会被缓存在mcentral中,而不是立刻释放归还给OS
2.2内存管理优化
对象分配是非常高频的操作:每秒分配GB级别的内存
小对象占比比较高
Go内存分配比较耗时
分配路径长:g -> m -> p -> mcache -> mspan -> memory block -> return pointer
pprof:对象分配的函数时最频繁调用的函数之一
字节跳动优化方案(Balanced GC):
每个g都绑定一大块内存(1KB),称做:goroutine allocation buffer(GAB)
GAB用于noscan类型的小对象分配:<128 B
使用三个指针维护GAB:base,end,top
Bump pointer(指针碰撞)风格对象分配
无需和其他分配请求互斥
分配动作简单高效
GAB对于Go内存管理来说是一个大对象
本质:将多个小对象的分配合并成一次大对象的分配
问题:GAB的对象分配方式会导致内存被延迟释放
从 Go runtime 内存管理模块的角度看,一个 allocation buffer 其实是一个大对象。本质上balanced GC 是将多次小对象的分配合并成一次大对象的分配。因此,当 GAB 中哪怕只有一个小对象存活时,Go runtime 也会认为整个大对象(即 GAB)存活。
当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中
原来的GAB可以释放,避免内存泄漏
本质:用copy GC 的算法管理小对象
为此,balanced GC 会根据 GC 策略,将 GAB 中存活的对象移动到另外的 GAB 中,从而压缩并清理 GAB 的内存空间,原先的 GAB 空间由于不再有存活对象,可以全部释放,如下图所示。
上图上方是两个 GAB,其中虚线表示 GAB 中对象的分界线。黑色表示 GAB 中存活的对象,白色表示死掉的对象。由于 GAB 中有存活对象,整个 GAB 无法被回收。
Balanced GC 会将 GAB 中存活的对象移动到下面的 GAB 中,这样原先的两个 GABs 就可以被释放,压缩并清理 GAB 的内存空间。
Balanced GC 只负责 noscan 对象的分配和移动,对象的标记和回收依然依赖 Go GC 本身,并和 Go GC 保持兼容。
3.编译器和静态分析
3.1编译器结构
他是一个重要的系统软件
识别符合语法和非法的程序
生成正确且高效的代码
分析部分(前端 front end)
词法分析,生成语素
语法分析,生成语法树
语义分析,收集类型信息,进行语义检查
中间代码生成,生成intermediate representation (IR)
综合部份(后端 back end)
代码优化,机器无关优化,生成优化后的IR
代码生成,生成目标代码
3.2静态分析
静态分析:不执行代码,推导程序的行为,分析程序的性质
控制流(Control flow):程序执行的流程
数据流(Data flow):数据在控制流上的传递
通过分析控制流和数据流,我们可以知道更多关于程序的性质
根据这些性质优化代码
数据流:
3.3过程内分析和过程间分析
Intra-procedural analysis: 过程内分析:在函数内进行控制流和数据流的分析
Inter-procedural analysis: 过程间分析:除了函数内的分析,还需要考虑跨函数的数据流和控制流,例如参数传递,函数返回值等
为什么过程间分析是个问题?
需要通过数据流分析得知i的具体类型,才知道i.foo()调用的是哪个foo()
根据 i 的具体类型,产生了新的控制流,i.foo(),分析继续
过程间分析需要同时分析控制流和数据流—联合求解,比较复杂
4.Go编译器优化
目的
- 用户无感知,重新编译即可获得性能收益
- 通用的优化手段
现状
- 采用的优化较少
- 追求编译时间短,因此没有进行复杂的代码分析和优化
思路
- 场景:面向后端长期执行的任务
- Tradeoff:用适当增加编译时间换取更高性能的代码
字节跳动用适当的编译时间换取更高性能的机器码
4.1函数内联(Inlining)
定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
-
优点
- 消除调用开销
- 将过程间分析的问题转换为过程内分析,帮助其他分析
-
缺点
- 函数体变大
- 编译生成的 Go 镜像文件变大
- 函数内联在大多数情况下是正向优化,即多内联,会提升性能
-
采取一定的策略决定是否内联
- 调用和被调用函数的规模
Beast Mode
-
Go 内联的限制
- 语言特性:interface, defer 等等,限制了内联优化
- 内联策略非常保守
-
字节跳动的优化方案
- 修改了内联策略,让更多函数被内联
- 增加了其他优化的机会:逃逸分析
-
开销
- Go 镜像大小略有增加
- 编译时间增加
- 运行时栈扩展开销增加
4.2逃逸分析
- 定义:分析代码中指针的动态作用域,即指针在何处可以被访问
-
大致思路
-
从对象分配处出发,沿着控制流,观察数据流。若发现指针 p 在当前作用域 s:
- 作为参数传递给其他函数;
- 传递给全局变量;
- 传递给其他的 goroutine;
- 传递给已逃逸的指针指向的对象;
-
则指针 p 逃逸出 s,反之则没有逃逸出 s.
-
Beast mode :函数内联扩展了函数边界,更多对象不逃逸
-
优化:未逃逸出当前函数的指针指向的对象可以在栈上分配
- 对象在栈上分配和回收很快:移动 sp 即可完成内存的分配和回收;
- 减少在堆上分配对象,降低 GC 负担。
参考阅读:
The Garbage Collection Handbook -- the art of automatic memory management
是自动内存管理领域的集大成之作。把自动内存管理的问题、动机、方案、以及最新研究进展和方向进行了非常详尽的阐述。整个书很好读,参考文献非常充实,推荐大家阅读英文版。
JEP 333: ZGC: A Scalable Low-Latency Garbage Collector openjdk.java.net/jeps/333
是目前 HotSpot JVM 上 pauseless GC 实现的 proposal,可以看作 GC 领域比较新的工程方面的进展。
数据密集型应用系统设计 Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems
通过例子带大家理解互联网产品需要解决的问题以及方案。
编译原理 The Dragon book, Compilers: Principles, Techniques, and Tools
在编译器前端着墨较多。本书第二版的第九章 机器无关优化,推荐大家反复仔细阅读。这一章主要讲述的是编译优化中常见的数据流分析问题,建议大家跟随书本仔细推导书中的例子,会帮助你对数据流分析有个大致的认识。这一章给出的引用文献大多是编译和静态分析领域非常有影响力的论文,有兴趣的同学可以阅读。
编译原理 Principles and Techniques of Compilers silverbullettt.bitbucket.io/courses/com…
南京大学编译原理课程。
静态程序分析 Static Program Analysis pascal-group.bitbucket.io/teaching.ht…
南京大学静态程序分析课程。参考文献 4 数据流分析读不懂的地方可以参考本课程的课件。
编译器设计 Engineering a Compiler
在编译器后端优化着墨较多。可以帮助大家理解后端优化的问题。
JVM Anatomy Quark #4: TLAB allocation shipilev.net/jvm/anatomy…
Goroutine allocation buffer (GAB) 的优化思路在 HotSopt JVM 也能找到类似的实现。
Constant folding, en.wikipedia.org/wiki/Consta…
常量折叠数据流分析。
Choi, Jong-Deok, et al. "Escape analysis for Java." Acm Sigplan Notices 34.10 (1999): 1-19.
逃逸分析的 Java 实现。
Zhao, Wenyu, Stephen M. Blackburn, and Kathryn S. McKinley. "Low-Latency, High-Throughput Garbage Collection." (PLDI 2022). 学术界和工业界在一直在致力于解决自动内存管理技术的不足之处,感兴趣的同学可以阅读。