自学内容网 自学内容网

高质量编程 & 性能优化学习笔记

高质量编程 & 性能优化学习笔记

目录

高质量编程

性能优化

自动内存管理

Go内存管理及优化

编译器和静态分析

Go编译器优化

高质量编程

编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码

  • 各种边界条件是否完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则

  • 简单性
    • 消除“多余的复杂性”,以简单清晰的逻辑编写代码
    • 不理解的代码无法修复改进
  • 可读性
    • 代码是写给人看的,而不是机器
    • 编写可维护代码的第一步是确保代码可读
  • 生产力
    • 团队整体工作效率非常重要

编码规范

代码格式

推荐使用gofmt自动格式化代码
goimports实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类

注释

公共符号始终要注释

  • 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
  • 任何既不明显也不简短的公共功能必须予以注释
  • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
  • 有一个例外,不需要注释实现接口的方法
  • 注释应该做的
    • 解释代码作用
    • 解释代码如何做的
    • 解释代码实现的原因
    • 解释代码什么情况会出错
命名规范
  • 简洁胜于冗长
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
    • 例如使用ServeHTTP而不是ServeHttp
    • 使用XMLHTTPRequest而不是xmlHTTPRequest
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨别出其含义

function:

  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
  • 函数名尽量简短
  • 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
  • 当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息

package:

  • 只由小写字母组成,不包含大写字母和下划线等字符
  • 简短并包含一定的上下文信息,例如schema、task等
  • 不要与标准库同名,例如不要使用sync或strings
    以下规则尽量满足,以标准库包名为例
  • 不使用常用变量名作为包名,例如使用bufio而不是buf
  • 使用单数而不是复数,例如使用encoding而不是encodings
  • 谨慎地使用缩写,例如使用fmt在不破坏上下文的情况下比format更加简短
控制流程
  • 避免嵌套,保持正常流程清晰
  • 尽量保持正常代码路径为最小缩进
    • 优先处理错误情况、特殊情况,尽早返回或继续循环来减少嵌套
  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • 正常流程代码沿着屏幕向下移动
  • 提升代码的可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环语句中
错误和异常处理

简单错误

  • 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
  • 优先使用errors.New()来创建匿名变量来直接表示简单错误
  • 如果有格式化的要求,使用fmt.Errorf()

错误的Wrap和Unwrap

  • 错误的Wrap实际上是提供一个error嵌套另一个error的能力,从而生成一个error的跟踪链
  • 在fmt.Errorf中使用%w关键字来讲一个错误关联至错误链中
    Go1.13在errors中新增了三个新API和一个新的format关键字,分别是errors.Is,errors.As,errors.Unwrap以及fmt.Errorf的%w,如果项目运行在小于Go1.13的版本中,导入golang.org/x/xerrors来使用

错误判定

  • 判定一个错误是否为特定错误,使用errors.Is,不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定错误
  • 在错误链上获取特定类型的错误,使用errors.As

panic

  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会导致程序崩溃
  • 若问题可以被屏蔽或解决,建议使用error代替panic
  • 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic

recover

  • recover只能在被defer的函数中使用
  • 嵌套无法生效
  • 只能在当前goroutine生效
  • defer语句是后进先出
  • 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈

error尽可能提供简明的上下文信息链,方便定位问题
panic用于真正异常的情况
recover生效范围:在当前goroutine的被defer的函数中生效

性能优化

性能优化:提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力
原因:

  • 用户体验:带来用户体验的提升
  • 资源高效利用:降低成本,提升效率

性能优化的层面:业务代码、SDK、基础库、语言运行时、OS

  • 业务层优化
    • 针对特定场景,具体问题具体分析
    • 容易获得较大性能效益
  • 语言运行时优化
    • 解决更通用的性能问题
    • 考虑更多场景
    • Tradeoffs
  • 数据驱动
    • 自动化性能分析工具–pprof
    • 依靠数据而非猜测
    • 首先优化最大瓶颈

性能优化与软件质量

  • 软件质量至关重要
  • 在保证接口稳定的前提下改进具体实现
  • 测试用例:覆盖尽可能多的场景,方便回归。用测试驱动开发
  • 文档:做了什么,没做什么,能达到怎样的效果
  • 隔离:通过选项控制是否开启优化
  • 可观测:必要的日志输出

性能优化建议

Benchmark
  • 性能表现需要实际数据衡量
  • Go语言提供了支持基准性能测试的benchmark工具
Slice
  • 尽可能在使用make()初始化切片时提供容量信息
  • 切片本质是一个数组片段的描述
    • 包括数组指针
    • 片段长度
    • 片段容量(不改变内存分配情况下的最大长度)
  • 切片操作并不复制切片指向的元素
  • 创建一个新的切片会复用原来切片的底层数组,在已有切片基础上创建切片,不会创建新的底层数组
  • 陷阱:大内存未释放
    • 原切片较大,代码在原切片基础上新建小切片
    • 原底层数组在内存中有引用,得不到释放
  • 可使用copy代替re-slice
Map
  • 尽可能在使用make()初始化切片时提供容量信息
  • 提前分配好空间可以减少内存拷贝和Rehash的消耗
字符串处理
  • 使用+拼接性能最差,string.Builder和bytes.Buffer相近,strings.Buffer更快
  • bytes.Buffer转化为字符串时重新申请了一块空间,strings.Builder直接将底层的[]byte转换为字符串类型返回
空结构体

使用空结构体节省内存

  • 空结构体struct{}实例不占据任何内存空间
  • 可作为各种场景下的占位符使用
    • 节省资源
    • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
  • 实现Set,可以考虑用Map来代替,开源实现set
  • 对于这个场景,只需要用到map的键,而不需要值
atomic包

使用atomic包

  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic操作是通过硬件实现,效率比锁高
  • sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}
建议
  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 普通应用代码不要一味地追求程序的性能
  • 越高级的性能优化手段越容易出现问题
  • 在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能

实战pprof

实战案例
golang pprof实战
Go性能分析工具

性能调优原则
  • 要依靠数据不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化
性能分析工具pprof
  • 希望知道应用在什么地方耗费了多少CPU、Memory
  • pprof是用于可视化和分析性能分析数据的工具

本实战是根据golang pprof实战一步步实现的,前提是下载项目代码,能够编译运行,会占用2CPU核心和超过2GB的内存,不要爆内存了

本实验使用的是vmware虚拟机下的Ubuntu系统,不同系统命令会有所不同

CPU占用过高

当我们开始运行main.go时

go run main.go

在浏览器打开 http://localhost:6060/debug/pprof/ 就可以看到以下页面
在这里插入图片描述

类型描述备注
allocs内存分配情况的采样信息可以用浏览器打开,但可读性不高
block阻塞操作情况的采样信息可以用浏览器打开,但可读性不高
cmdline显示程序启动命令及参数可以用浏览器打开,这里会显示 ./go-pprof-practice
goroutine当前所有协程的堆栈信息可以用浏览器打开,但可读性不高
heap堆上内存使用情况的采样信息可以用浏览器打开,但可读性不高
mutex锁争用情况的采样信息可以用浏览器打开,但可读性不高
profileCPU占用情况的采样信息浏览器打开会下载文件
threadcreate系统线程创建情况的采样信息可以用浏览器打开,但可读性不高
trace程序运行跟踪信息浏览器打开会下载文件

另开一个终端输入top命令查看CPU占用,main.go要一直运行着

top

在这里插入图片描述

命令行输入以下命令会出现一个Fetching…,需要等待一会然后返回信息,就成功打开pprof了

go tool pprof http://localhost:6060/debug/pprof/profile

在这里插入图片描述

在终端输入以下命令查看CPU占用较高的调用

top

在这里插入图片描述

pprof名词解释:

  • flat:当前函数本身的执行耗时
  • flat%:flat占CPU总时间的比例
  • sum%:上面每一行的flat%总和
  • cum:指当前函数本身加上其调用函数的总耗时
  • cum%:cum占CPU总时间的比例

flat == cum:说明当前函数没有调用其他函数
flat == 0:说明函数中只有其他函数的调用

显然发现CPU占用较高都是由github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat造成的

输入以下命令查看问题具体在代码的什么位置

list Eat

通过返回信息,发现问题所在,即可去对应的代码里修改
在这里插入图片描述

使用web命令可以生成图片,越大越红的说明占用资源越多,不过本菜鸡在实操过程中一直不能使用在pprof里的graphviz,但确实已经install了graphviz,非常奇怪,但也并不太妨碍后续的分析,graphviz只是一个图片生成器,生成一个.svg的文件

内存占用过高

通过任务管理器我们发现,main.go仍然占用着巨大的内存空间,下面来排查内存空间问题
在这里插入图片描述
在终端输入

go tool pprof http://localhost:6060/debug/pprof/heap

再次在pprof内使用top指令找到问题代码
在这里插入图片描述

在pprof内使用list Steal查看代码出现问题的代码段
在这里插入图片描述

可以看到下面代码一直在追加1MB数组,直到达到1GB为止

m.buffer = append(m.buffer, [constant.Mi]byte{})

找到相应文件,修改问题代码段

排查内存频繁回收

接下来是排查频繁内存回收
先暂停炸弹文件执行,输入以下指令获取运行过程中的GC日志,然后运行main.go,发现程序在不断地申请16MB然后回收到0MB

GODEBUG=gctrace=1 ./go-pprof-practice | grep gc

在这里插入图片描述

继续使用pprof排查问题

go tool pprof http://localhost:6060/debug/pprof/allocs

在pprof内输入top指令和list Run指令,找到问题代码段
在这里插入图片描述
在这里插入图片描述

这里有个小插曲,你可尝试一下将16 * constant.Mi修改成一个较小的值,重新编译运行,会发现并不会引起频繁GC,原因是在golang里,对象是使用堆内存还是栈内存,由编译器进行逃逸分析并决定,如果对象不会逃逸,便可在使用栈内存,但总有意外,就是对象的尺寸过大时,便不得不使用堆内存。所以这里设置申请16MB的内存就是为了避免编译器直接在栈上分配,如果那样得话就不会涉及到GC了。

排查协程泄露

由于golang自带内存回收,所以一般不会发生内存泄露。但凡事都有例外,在golang中,协程本身是可能泄露的,或者叫协程失控,进而导致内存泄露。

运行一段时间炸弹程序后,发现协程数量达到了115
在这里插入图片描述

继续使用pprof排查问题

go tool pprof http://localhost:6060/debug/pprof/goroutine

依然是toplist Drinkweb三板斧,由于我的问题就不展示webQAQ
在这里插入图片描述
在这里插入图片描述

根据前两板斧发现,github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Drink.func1一直在创建没有用的协程
可以看到,Drink函数每次会调用10个协程出去,每个协程会睡眠30秒再退出,而Drink函数又会被反复调用,这才导致大量协程泄露。
试想一下,如果释放出的协程会永久阻塞,那么泄露的协程数便会持续增加,内存的占用也会持续增加,那迟早是会被操作系统杀死。
当我们修改好问题代码段后,协程数量变少且稳定
在这里插入图片描述

排查锁的争用

到目前为止,我们已经解决这个炸弹程序的所有资源占用问题,但是事情还没有完,我们需要进一步排查那些会导致程序运行慢的性能问题,这些问题可能并不会导致资源占用,但会让程序效率低下,这同样是高性能程序所忌讳的。

由上图我们发现协程数量是变少了,但还存在一个mutex的问题,下面我们来解决这个问题
继续执行程序,然后进入pprof排查问题

go tool pprof http://localhost:6060/debug/pprof/mutex

然后再掏出我的两板斧toplist,定位问题代码段
在这里插入图片描述
在这里插入图片描述

简单分析一下问题代码段,首先创建一个sync锁,运行时锁住,然后执行了一个协程,协程先睡眠一秒,然后再解锁,此时程序会卡在第60行的位置,即程序需要再次锁住,但它必须等待子协程睡眠一秒后解锁,因此白白浪费了时间

排查阻塞操作

修改完成以上所有问题后再运行代码,发现还存在两个阻塞,虽然可能不是问题,但为了保证性能还是要排查一下
在这里插入图片描述

接下来我们来排查一下阻塞操作,继续使用我们的pprof

go tool pprof http://localhost:6060/debug/pprof/block

在这里插入图片描述
在这里插入图片描述

这里的问题不是睡眠一秒,而是从一个channel里读取数据时,发生了阻塞,直到这个channel在一秒后才有数据读出,导致程序阻塞了一秒而非睡眠了一秒

至此,炸弹实验到此结束,完结撒花!美中不足的是我没配置好web命令

名词解释

  • alloc_objects:程序累计申请的对象数
  • alloc_space:程序累计申请的内存大小
  • inuse_objects:程序当前持有的对象数
  • inuse_space:程序当前占用的内存大小

在这里插入图片描述

CPU采样过程和原理
  • 采样对象:函数调用和它们占用的时间
  • 采样率:100次/秒,固定值
  • 采样时间:从手动启动到手动结束
  • 操作系统每10ms向进程发送一次SIGPROF信号,进程每次接收到SIGPROF会记录调用堆栈,写缓存每100ms读取已经记录的调用栈并写入输出流
Heap采样过程和原理
  • 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
  • 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
  • 采样时间:从程序运行开始到采样时
  • 采样指标:alloc_space、alloc_objects、inuse_space、inuse_objects
  • 计算方式:inuse=alloc-free
Goroutine协程 & ThreadCreate线程创建
  • Goroutine
    • 记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)runtime.main的调用栈信息
  • ThreadCreate
    • 记录程序创建的所有系统线程的信息
Block阻塞 & Mutex锁
  • 阻塞操作
    • 采样阻塞操作的次数和耗时
    • 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均记录
  • 锁竞争
    • 采样争抢锁的次数和耗时
    • 采样率:只记录固定比例的锁操作,1为每次加锁均记录

性能调优案例

  • 业务服务优化
  • 基础库优化
  • Go语言优化
业务服务优化

基本概念

  • 服务:能单独部署,承载一定功能的程序
  • 依赖:Service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B
  • 调用链路:能支持一个接口请求的相关服务集合及其互相之间的依赖关系
  • 基础库:公共的工具包、中间件
    在这里插入图片描述

流程

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈
  • 重点优化项改造
  • 优化效果验证

建立服务性能评估手段

  • 服务性能评估方式
    • 单独benchmark无法满足复杂逻辑分析
    • 不同负载情况下性能表现差异
  • 请求流量构造
    • 不同请求参数覆盖逻辑不同
    • 线上真实流量情况
  • 压测范围
    • 单机器压测
    • 集群压测
  • 性能数据采集
    • 单机性能数据
    • 集群性能数据

分析性能数据,定位性能瓶颈

  • 使用库不规范
  • 高并发场景优化不足

重点优化项改造

  • 正确性是基础
  • 响应数据diff
    • 线上请求数据录制回放,把线上的请求录制下来,等优化好了以后在新服务上重新回放请求数据检验效果,差异不大的话说明对服务的功能是没有影响的
    • 新旧逻辑接口数据diff

优化效果验证

  • 重复压测验证
  • 上线评估优化效果
    • 关注服务监控
    • 逐步放量
    • 收集性能数据

进一步优化,服务整体链路分析

  • 规范上游服务调用接口,明确场景需求
  • 分析链路,通过业务流程优化提升服务性能(请求合并,增加写缓存,减小数据集等)
基础库优化

适用范围更广

AB实验SDK的优化

  • 分析基础库核心逻辑和性能瓶颈
    • 设计完善改造方案
    • 数据按需获取
    • 数据序列化协议优化
  • 内部压测验证
  • 推广业务服务落地验证
Go语言优化

编译器&运行时优化

  • 优化内存分配策略
  • 优化代码编译流程,生成更高效的程序
  • 内部压测验证
  • 推广业务服务落地验证
  • 优点:
    • 接入简单,只需要调整编译配置
    • 通用性强

自动内存管理

术语前瞻:

  • Auto memory management: 自动内存管理
  • Grabage collction: 垃圾回收
  • Mutator: 业务线程,分配新对象,修改对象指向关系
  • Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
  • Serial GC:只有一个collector的GC算法,把所有线程先暂停,然后执行一个collector做垃圾回收
  • Parallel GC: 并行 GC,支持多个collectors回收的GC算法,把所有线程先暂停,然后执行多个collector做垃圾回收
  • Concurrent GC: 并发 GC,mutator(s)和collector(s)可以同时执行,不需要把所有线程都暂停,可以一边执行线程一边做垃圾回收
    • Collectors必须感知对象指向关系的改变
  • Tracing garbage collection: 追踪垃圾回收
    • Copying GC: 复制对象 GC
    • Mark-sweep GC: 标记-清理 GC
    • Mark-compact GC: 标记-压缩 GC
  • Reference counting: 引用计数
  • Generational GC: 分代 GC
    • Young generation: 年轻代
    • Old generation: 老年代
动态内存
  • 程序在运行时根据需求动态分配的内存:malloc()
    自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存
  • 避免手动内存管理,专注于实现业务逻辑
  • 保证内存使用的正确性和安全性:double-free problem,use-after-free problem
    三个任务
  • 为新对象分配空间
  • 找到存活对象
  • 回收死亡对象的内存空间
评价GC算法
  • 安全性(Safety):不能回收存活对象(基本要求)
  • 吞吐率(Throughput): 1 − G C 时间 程序执行总时间 1-\frac{GC时间}{程序执行总时间} 1程序执行总时间GC时间(花在GC上的时间,一般追求吞吐率高一点)
  • 暂停时间(Pause time):stop the world(STW,业务是否有感知)
  • 内存开销(Space overhead)GC元数据开销
追踪垃圾回收(Tracing garbage collection)
  • 对象被回收的条件:指针指向关系不可达的对象
  • 标记根对象
    • 静态变量、全局变量、常量、线程栈等都是根对象
  • 标记:找到可达对象
    • 求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
  • 清理:所有不可达对象
    • 将存活对象复制到另外的内存空间(Copying GC):将对象复制到另外的内存空间
    • 将死亡对象的内存标记为"可分配"(Mark-sweep GC):使用free list管理空闲内存
    • 移动并整理存活对象(Mark-compact GC):原地整理对象
  • 根据对象的生命周期,使用不同的标记和清理策略

常见内存管理方式

分代GC(Generational GC)
  • 分代假说(Generational hypothesis):most objects die young
  • Intuition:很多对象在分配出来后很快就不再使用了
  • 每个对象都有年龄:经历过GC的次数
  • 目的:对年轻和老年的对象,指定不同的GC策略,降低整体内存管理的开销
  • 不同年龄的对象处于heap的不同区域
  • 年轻代(Young generation)
    • 常规的对象分配
    • 由于存活对象很少,可以采用copying collection
    • GC吞吐率很高
  • 老年代(Old generation)
    • 对象趋向于一直活着,反复复制开销很大
    • 可以采用mark-sweep collection
引用计数(Reference counting)

每个对象都有一个与之关联的引用数目
对象存活的条件:当且仅当引用数大于0

优点:

  • 内存管理的操作被平摊到程序执行过程中
  • 内存管理不需要了解runtime的实现细节,只需要维护引用计数:C++智能指针(smart pointer)
    缺点:
  • 维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性,原子操作一般开销较大
  • 无法回收环形数据结构–解决办法:weak reference
  • 内存开销:每个对象都引入的额外内存空间存储引用数目
  • 回收内存时依然可能引发暂停,尽管内存管理的操作已经平摊到程序执行过程中,但在回收一些大型数据结构时依然会引发暂停

学术界和工业界一直在致力于解决自动内存管理技术的不足之处PLDI2022 Low-Latency, High-Throughput Garbage Collection

Go内存管理及优化

术语前瞻

  • TCMalloc
  • mmap() 系统调用
  • scan object 和 noscan object
  • mspan, mcache, mentral
  • Bump-pointer object allocation: 指针碰撞风格的对象分配

Go内存分配

分块

目标:为对象在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,会根据一定策略归还给OS

Go内存管理优化

对象分配是非常高频的操作:每秒分配GB级别的内存
小对象占比较高
Go内存分配比较耗时

  • 分配路径长:g->m->p->mcache->mspan->memory block->return pointer
  • pprof:对象分配的函数是最频繁调用的函数之一
Balanced GC

每个g都绑定一大块内存(1KB),称作goroutine allocation buffer(GAB)
GAB用于noscan类型的小对象分配:< 128B
使用三个指针维护GAB:base,end,top
Bump pointer(指针碰撞)风格对象分配

  • 无须和其他分配请求互斥
  • 分配动作简单高效

在这里插入图片描述
在这里插入图片描述

GAB对于Go内存管理来说是一个大对象
本质:将多个小对象的分配合并成一次大对象的分配
问题:GAB的对象分配方式会导致内存被延迟释放
方案:移动GAB中存活的对象

  • 当GAB总大小超过一定阈值时,对GAB进行清理操作,将GAB中存活的对象复制到另外分配的GAB中
  • 将原先的GAB可以释放,避免内存泄漏
  • 本质:用copying GC的算法管理小对象,根据对象的生命周期,使用不同的标记和清理策略

编译器和静态分析

术语前瞻

  • 词法分析
  • 语法分析
  • 语义分析
  • Intermediate representation (IR) 中间表示
  • 代码优化
  • 代码生成
  • Control flow: 控制流
  • Data flow: 数据流
  • Intra-procedural analysis 过程内分析
  • Inter-procedural analysis: 过程间分析

编译器

编译器结构

在这里插入图片描述

重要的系统软件

  • 识别符合语法和非法的程序
  • 生成正确且高效的代码
    分析部分(前端front end)
  • 语法分析,生成词素(lexeme)
  • 语法分析,生成语法树
  • 语义分析,收集类型信息,进行语义检查
  • 中间代码生成,生成intermediate representation(IR)
    综合部分(后端back end)
  • 代码优化,机器无关优化,生成优化后的IR
  • 代码生成,生成目标代码

静态分析

静态分析:不执行程序代码,推导程序的行为,分析程序的性质
控制流(Control flow):程序执行的流程,使用控制流图(Control-flow graph)表示控制流
数据流(Data flow):数据在控制流上的传递
通过分析控制流和数据流,我们可以知道更多关于程序的性质(properties),根据这些性质优化代码

过程内分析(intra-procedural analysis):仅在函数内部进行分析
过程间分析(inter-procedural analysis):考虑函数调用时参数传递和返回值的数据流和控制流
为什么过程间分析是个问题?分析以下代码i.foo()是A.foo()还是B.foo()

  • 需要通过数据流分析得知i的具体类型,才知道i.foo()调用的是哪个foo()
  • 根据i的具体类型,产生了新的控制流,i.foo(),分析继续
  • 过程间分析需要同时分析控制流和数据流,联合求解,比较复杂
type I interface {
    foo()
}

type A struct {}
type B struct {}

func (a *A) foo() {

}

func (b *B) foo() {

}

func bar() {
    //i = &A{}  //一定是A.foo()
    
    i.foo()
}

Go编译器优化

术语前瞻

  • Function inlining: 函数内联
  • Escape analysis: 逃逸分析

为什么做编译器优化

  • 用户无感知,重新编译即可获得性能收益
  • 通用性优化

现状

  • 采用的优化少
  • 编译时间较短,没有进行较复杂的代码分析和优化

编译优化的思路

  • 场景:面向后端长期执行的任务
  • Tradeoff:用编译时间换取更高效的机器码

Beast mode集成在SDK内

  • 函数内联
  • 逃逸分析
  • 默认栈大小调整
  • 边界检查消除
  • 循环展开
  • ···

函数内联

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

优点

  • 消除函数调用开销,开销例如传递参数、保护寄存器等
  • 将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析

使用micro-benchmark快速验证和对比性能优化结果

缺点

  • 函数体变大,instruction cache(icache)不友好
  • 编译生成的Go镜像变大

函数内联在大多数情况下都是正向优化

内联策略

  • 调用和被调函数的规模,如果caller已经很大了,就不把callee引用进来了
  • ···
Beast Mode

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

  • 语言特性,例如interface,defer等,限制了函数内联
  • 内联策略非常保守

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

  • 降低函数调用的开销
  • 增加了其他优化的机会:逃逸分析

开销

  • Go镜像增加~10%
  • 编译时间增加

逃逸分析

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

大致思路

  • 从对象分配处出发,沿着控制流,观察对象的数据流
  • 若发现指针p在当前作用域s:
    • 如果这个p作为参数传递给其他函数
    • 或传递给全局变量
    • 或传递给其他的goroutine
    • 或传递给已逃逸的指针指向的对象
  • 则指针p指向的对象逃逸出s,反之则没有逃逸出s
  • 如果指针p能在其他地方直接或间接访问,则已经出现逃逸

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

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

  • 对象在栈上分配和回收更快:移动sp
  • 减少在heap上的分配,降低GC负担

原文地址:https://blog.csdn.net/eyuhaobanga/article/details/145241210

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!