这是我参与「第五届青训营 」笔记创作活动的第4天。
经过上节课的性能分析实战,我们对影响Go语言性能的一些反面案例有了一定的认识,并且学习使用pprof工具进行了代码性能调优实战。今天老师分享了对Go语言的内存和编译器的优化思路,下面是对课后习题的一个记录。
-
从业务层和语言运行时层进行优化分别有什么特点?
- 业务层优化
- 针对特定场景,具体问题,具体分析,比如通过工具pprof,发现线上项目存在性能瓶颈,进而进行优化
- 容易获得较大性能收益
- 语言运行时优化
- 解决更通用的性能问题
- 考虑更多场景
- Tradeoffs (对于通用性能问题和具体项目性能问题的平衡)
- 业务层优化
-
从软件工程的角度出发,为了保证语言 SDK 的可维护性和可拓展性,在进行运行时优化时需要注意什么?
- 软件质量至关重要
- 在保证接口稳定的前提下改进具体实现
- 因为优化SDK之前已经存在一些稳定的接口以及很多方法依赖该接口,如果我们改变了接口行为,会造成不便
- 测试用例:覆盖尽可能多的场景,方便回归
- 文档:做了什么,没做什么,能达到怎样的效果
- 隔离:通过选项控制是否开启优化
- 可观测:必要的日志输出
-
自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点有哪些?
- 动态内存
- 程序在运行时根据需求动态分配的内存:malloc()
- 优点 :手动管理内存,减少语言运行时对内存的耗时操作
- 缺点 :容易引发内存操作错误
- 自动内存管理 (垃圾回收 GC):由程序语言的运行时系统管理动态内存
- 避免手动内存管理,专注于实现业务逻辑
- 保证内存使用的正确性和安全性:double-free problem,use-after--free problem
- 动态内存
-
什么是分代假说?分代 GC 的初衷是为了解决什么样的问题?
- 分代假说
- most objects die young
- 对于全局变量,程序中局部临时变量更多,生命周期更短
- most objects die young
- 目的
- 针对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
- 分代假说
-
Go 是如何管理和组织内存的?
- 目标:为对象在heap上分配内存
- 提前将内存分块
- 调用系统调用mmap()向OS申请一大块内存,例如4MB
- 先将内存划分成大块,例如8KB,称作mspan
- 再将大块继续划分成特定大小的小块,用于对象分配
- noscan mspan:分配不包含指的对象 一 GC不需要扫描
- scan mspan:分配包含指针的对象 一 GC需要扫描
- 对象分配:根据对象的大小,选择最合适的块返回
-
为什么采用 bump-pointer 的方式分配内存会很快?
- 无须和其他分配请求互斥
- 分配动作简单高效
-
为什么我们需要在编译器优化中进行静态代码分析?
- 静态分析
- 不执行代码,推导程序的行为,分析程序的性质
- 控制流(Control flow):程序执行的流程
- 数据流(Data flow):数据在控制流上的传递
- 通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties),并根据这些性质来优化代码
- 静态分析
-
函数内联是什么,这项优化的优缺点是什么?
- 内联:将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
- 优点
- 消除函数调用开销,例如传递参数、保存寄存器等
- 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
- 缺点
- 函数体变大,instruction cache(icache)不友好,如递归调用一个内联函数,函数体会变得很大
- 编译生成的Go镜像变大
-
什么是逃逸分析?逃逸分析是如何提升代码性能的?
- 逃逸分析:分析代码中指针的动态作用域:指针在何处可以被访问
- 大致思路
- 从对象分配处出发,沿着控制流,观察对象的数据流
- 若发现指针p在当前作用域s:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的goroutine
- 传递给已逃逸的指针指向的对象
- 则指针·指向的对象逃逸出,反之则没有逃逸出
- Beast mode:函数内联拓展了函数边界,更多对象不逃逸
- 优化:末逃逸的对象可以在栈上分配
- 对象在栈上分配和回收很快:移动sp
- 减少在heap上的分配,降低GC负担