[Go内存管理&编译器优化|青训营笔记]

100 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第5天

一、GO内存支配——分块

目标:为对象在heap.上分配

内存

提前将内存分块

1.调用系统调用mmapQ)向OS申请大块内存,例如4 MB

2.先将内存划分成大块,例如8 KB,称作mspan

3.再将大块继续划分成特定大小的小块,用于对象分配

4.noscan mspan:分配不包含指针的对象一GC 不需要扫描

5.scan mspan:分配包含指针的对象—GC需要扫描

对象分配:根据对象的大小,选择最合适的块返回

二、 Go内存分配-缓存

1.TCMalloc thread caching

2.每个p包含一个mcache用于快速分配,用于为绑定于p上的g分配对象

3.mcache管理一组mspan

4.当mcache中的mspan分配完毕,向mcentral申请带有末分配块的mspan

5.当mspan中没有分配的对象,mspan 会被缓存在mcentral中,而不是立刻释放并归还给OS

GO 内存管理优化

1.对象分配是非常高频的操作:每秒分配GB级别的内存

2.小对象占比较高

3.Go内存分配比较耗时

(1)分配路径长:g->m->p->mcache->mspan->memory block->return pointer(分配路径很长,非常消耗CPU)

(2)pprof:对象分配的函数是最频繁调用的函数之一

优化方案:Balanced GC

1.每个g都给绑定一大块内存(1KB),称作 goroutine allocation buffer (GAB)

2.GAB用于noscan类型的小对象分配:<128B

3.使用三个指针维护GAB:base,end,top

4.Bump pointer(指针碰撞)风格对象分配

(1)无须和其他分配请求互斥

(2)分配动作简单高效

屏幕截图 2023-01-19 221727.png

if top + size <=end{
    adder:=top
    top+=size
    return adder
}

GAB对于Go内存管理来说是一个大对象

本质:将多个小对象的分配合并成一次大对象的分配

问题:GAB的对象分配方式会导致内存被延迟释放

方案:移动GAB中存活的对象

1.当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中

2.原先的GAB可以释放,避免内存泄漏

3.本质:用copying GC的算法管理小对象

屏幕截图 2023-01-19 213311.png

屏幕截图 2023-01-19 213550.png

Balanced GC——性能收益

高峰期CPU usage降低4.6%,核心接口时延下降4.5%—7.7%

总结

1.Go内存管理—分块

2.Go内存管理—缓存

3.Go对象分配的性能问题

(1)分配路径过长

(2)小对象居多

4.Balanced GC

(1)指针碰撞风格的对象分配

(2)实现了copying GC

(3)性能收益

三、编译器和静态分析

编译器的结构

1.重要的系统软件

(1)识别符合语法和非法的程序

(2)生成正确且高效的代码

2.分析部分(前端 front end)

(1)词法分析,生成词素(lexeme)

(2)语法分析,生成语法树

(3)语义分析,收集类型信息,进行语义检查

(4)中间代码生成,生成 intermediate representation(IR)

3.综合部分(后端 back end)

(1)代码生成,机器无关优化,生成优化后的IR

(2)代码生成,生成目标代码

静态分析

1.静态分析:不执行程序代码,推导程序行为,分析程序性质

  1. 控制流(Control flow):程序执行的流程

3.数据流(Data flow):数据在控制流上的传递

4.通过夫人逆袭控制流和数据流,我们可以知道更多关于程序的性质(properties)

5.根据这些性质优化代码

过程类和过程间的分析

1.过程内分析(Intra-procedural analysis)

(1)仅在过程内部进行分析

2.过程间分析(Inter-procedual analysis)

(1)考虑过程调用时参数传递和返回值的数据流和控制流

3.为什么过程间分析是个问题

(1)需要通过数据流分析得知 i 的具体类型,才能知道 i,foo() 调用的是哪个foo()

(2)根据 i 的具体类型,产生了新的控制流,i foo(),分析继续

(3)过程间分析需要同时分析控制流和数据流——联合求解,比较复杂

四、Go编译器优化

1.为什么做编译器优化

(1)用户无感知,重新编译即可获得性能收益

(2)通用性优化

2.现状

(1)采用的优化少

(2)编译时间较短,没有进行较复杂的代码分析和优化

3.编译优化的思路

(1).场景:面向后端长期执行任务

(2)Tradeoft:用编译时间换取更高效的机器码

4.Beast mode

(1)函数内联

(2)逃逸分析

(3)默认栈大小调整

(4)边界检查消除

(5)循环展开 等等

函数内敛(Inlining)

1.内联:

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

2.优点

(1)消除函数调用开销,例如传递参数、保存奇存器等

(2)将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析

3.缺点

(1)函数体变大,instruction cache (icache)不友好

(2)编译生成的Go镜像变大

4.函数内联能多大程度影响性能? ——使用 micro-benchmark验证一下

屏幕截图 2023-01-19 210439.png

Inlining优化后函数被内联后,性能数据提升 4.58倍

5.内联策略

调用和被调用函数的规模

Beast Mode

1.Go函数内联受到的限制较多

(1)语言特性,例如interface, defer等,限制了函数内联

(2)内联策略非常保守

2.Beast mode:调整函数内联的策略,使更多函数被内联

(1)降低函数调用的开销

(2)增加了其他优化的机会:逃逸分析

3.开销

(1)Go镜像增加~10%

(2)编译时间增加

逃逸分析

1.逃逸分析: 分析代码中指针的动态作用域:指针在何处可以被访问

2.大致思路

(1)从对象分配处出发, 沿着控制流,观察对象的数据流

(2)若发现指针p在当前作用域S:

(2.1)参数传递给其他函数

(2.2)传递给全局变量

(2.3)传递给其他的goroutine

(2.4)传递给已逃逸的指针指向的对象

(3)则指针p指向的对象逃逸出S,反之则没有逃逸出S

3.Beast Mode:函数内联拓展了函数边界,更多对象不逃逸

4.优化:未逃逸的对象可以在栈上分配

(1)对象在栈上分配和回收很快:移动sp

(2)减少在heap上的分配,降低GC负担

总结

1.Go编译器优化问题

2.Beast Mode

3.函数内联

4.逃逸分析

5.通过 micro—benchmark

6.性能收益

五、课程总结

1.本节课程:高性能Go原因呢发行版优化与落地实践

2.性能优化

(1)自动内存管理

(2)Go内存管理

(3)编译器与静态f分析

(4)编译器优化

3.实践

(1)Balanced GC 优化对象分配

(2)Beast mode 提升代码性能

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