这是我参与 [第三届青训营 -后端场]笔记创作活动的第6篇笔记
性能优化
what
特定场景下的优化
组件间:不必要的消耗减少
减少io的延迟
why
降低成本
Cycle
业务层优化
语言运行时优化
数据驱动
大瓶颈优先
优化原则
优化保证接口稳定性
测试代码会比优化代码多
日志输出:优化打开了,有收益
自动内存管理
正确性,安全性: 多次释放同一内存,使用以释放的内存
三个任务
相关概念
-
Mutator作用:
-
Collecter: GC线程
-
算法(GC执行方式)
- 1
- 2
- 3
为什么业务线程,是分配新对象...?
同时执行,挑战..
新增对象标记存活.. 混合写屏障,三色标记,需要的时候才唤起collector,不用就休眠
评价GC算法
- 安全性(Safety):存活对象不回收
- 吞吐率(Throughtput):花在GC时间上稍
- 暂停时间(Pause time):业务是否感知
- 内存开销(Space overhead):GC元数据开销
两种技术/算法
追踪垃圾回收
-
从栈,全局变量标记堆跟对象
-
从堆对象找指针指向对象
-
清理不可达对象
- 复制可达对象,其他全删了
- 标记不可达对象
- 同一块内存,将可达对象放到一起(指针:top) 原地复制
-
不同生命周期(可达/存活)对象,所用的策略不同
-
分代GC(为了分类不同生命周期的对象)
-
年轻: 函数内对象
- 数量少,采用复制年轻代对象,复制开销小,频繁引用开销大(存疑)
- 吞吐率高(花点时间占比少) 数量少
-
老: 类对象
- 使用复制开销大,一直活着,每一次垃圾回收都复制?(标记就好了)
- 碎片对象多,那就压缩🗜内存
-
引用计数
weak reference (解决环形数据结构回收问题)
Go内存管理及优化
Go内存分配
分块 Block
mmap:大块neicun
mspan: 每一列(8kb)
mspan中分为多个(特定大小)块(这里的块就可以放对象了)
缓存 Cache
goruntine:
m
p:
cache 不够用,向 central申请mspan(交换/满的放到mcentral)
Go内存管理优化(字节方案)
-
小对象多(<80b)
-
方案:GAB(NoScan <128b)
-
小对象合并为大对象
-
为什么小对象可以不等大?
-
小对象导致大对象存活
-
将散的对象放到一起(copy GC)
-
没有Goruntine指向/归属没关系吗?
-
放到其他位置,散的对象(就没有指针指向他)在下一轮GC中,就被回收了(回收的是原gab的内存(gab是总大小超过阈值,再清理)
- 问题: 在这一次还有指针指向,如果移动的话,不会造成指针指向未分配内存吗? (什么场景下协程里的许多对象会无指向,而存活少数对象?什么时候会进行垃圾回收?)
- 修改指针(对象移动,修改指针)
- noscan:可以理解为书的叶节点
-
每一个用户的gorountine都有一个gab./像GC这种gorountine,不需要gorountine
-
对象的大小记录在哪呢?
- 额外内存空间(0/1:开头)bmap
-
-
Go的对象没有对象头/Java的对象有对象头,里面有数据类型等数据标记起始和大小
Survivor Gab:不属于任何内存(有对象活着,就有指针指向这个内存块)
语言顶级会议: paper 论文作者主页 pldi ecrob
mcache:管理各个不同大小mspans
science classes on Go
编译器和静态分析
基本介绍
静态分析: 不执行代码,推导行为,性质
编译器结构/编译流程
数据流和控制流
- 控制流:分析程序执行的流程
- 数据流:分析数据在控制流上的传递(结果必为4,则直接返回4即可)
过程内/过程间分析
-
过程内分析
-
过程间分析
- 到底是哪一块内存?(需要同时分析数据流/控制流) 函数内联可避免过多过程间的分析
Go编译器优化(!!!)
背景
Go原生语言目标: 编译时间短,但...
目的:牺牲编译时间换取更高效的机器码
Beast Mode
- 函数内联
- 逃逸分析
- 默认栈大小调整
- 边界检查消除
- 循环展开
- ...
函数内联(Inlining)
内联: 将两个函数合并为一个函数(将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
真不愧牺牲编译时间
优点:将过程间分析转化为过程内分析(因为都是在同一个内存块内,不需要参数的传递),降低函数调用开销,增加了其他优化机会(逃逸分析)
性能提升: micro-benchmark快速验证性能优化结果(4.58x)
缺点: 函数体变大(instruction cache 复制 不友好),编译生成的Go镜像变大
递归就不内联啦(caller和callee大小策略决定是否内联)
Beast Mode
为什么 interface defer 限制了函数内联?
逃逸分析
概述:分析代码中指针的动态作用域:指针在何处可以被访问
如何理解指针p指向对象逃逸出当前作用域s?
p指向对象可以不通过p指针访问,辣么就可以说p指向对象逃逸出了指针p的作用域s(
作用域:指针p所在代码块(函数,对象)
内联与逃逸分析
若内联一层,则一层间函数调用(callee其实在caller里面)不会造成对象逃逸
优化: 未逃逸的对象可以在栈上分配 栈空间不是很小? 栈不是就存放指针吗? 对象分配不是分配内存吗? 是. 逃逸对象为什么不能在栈上分配
为什么 会分堆和栈? 堆的特点? 栈的特点?
- 对象在栈上分配和回收很快: 移动sp
- 减少在heap上的分配,减轻GC负担
Q&A
业务线程和Gorountine
一样的(已知)
GO GC阶段
- 扫描Goruntine(非全局)
课后作业
-
从业务层和语言运行时层进行优化分别有什么特点?
- 业务层:特定场景,提升明显
- 运行时层: 通用场景,提升相对不明显
-
从软件工程的角度出发, 为了保证语言SDK的可维护性和可拓展性,在进行以运行时优化需要注意什么?
- 需要保证向上提供的接口的稳定性
- 给用户选择开关
- 写好文档,说明作用和说明情况会出问题,做了哪些改动
- 运行信息记录到日志中
-
自动内存管理技术从大类上分为哪两种,每一种技术的特点以及优缺点?
-
引用计数
- 用户goruntine即可
- 原子化操作,对底层资源请求
-
追踪垃圾回收
- GC线程
-
-
什么是分代假说? 为了解决什么问题?
- 对象按经历垃圾回收次数分为 年轻代/年老代
- 为了解决,不同对象的区分(存活周期不同: 全局变量/局部变量)
-
Go是如何管理和组织内存?
- 预先分块(mspan)
- 多级缓存
-
为什么采用 bump-pointer的方式分配内存会很快?
- 因为减少了向mcache申请内存的次数(每一次申请都是大内存)
-
为什么我们需要在编译器优化中进行静态分析?
-
函数内联 what? 优缺点?
- 被调用函数体直接复制到主函数体内, 运行时快(不用函数调用,寄存器,不用拿个指针指来指去)
-
逃逸分析 what? 如何提升性能?
- 分析对象是否逃离出当前作用域.
- 未逃离的对象,可以在栈上分配内存.
\