所有的代码转化为可执行文件,都需要通过编译器将高级语言转化为计算器能够识别的低级语言。这个过程复杂且关键,很大程度上影响这门语言的性能。
关于编译器的优化工作也一直是人们研究的重点。但是,编译的过程涉及的知识过多,很多时候我们并不明白编译的过程中到底执行了什么操作。
本文通过分析Go编译器优化的完整案例,向大家分享编译器的编译规则的优化方法。
摘自OptimizeLab: https://github.com/OptimizeLab/docs
作者:surechen
编译器的作用是将高级语言的源代码翻译为低级语言的目标代码。通常为了便于优化处理,编译器会将源代码转换为中间表示形式(Intermediate representation),很多编译优化过程都是作用在这个形式上,如下面将介绍的通过给编译器添加编译规则优化性能。
在编译Go语言代码时通常使用Go语言编译器,它包括语法分析、AST变换、静态单赋值SSA PASS、机器码生成等多个编译过程。其中在生成SSA中间表示形式后进行了多个编译优化过程PASS,每个PASS都会对SSA形式的函数做转换,比如deadcode elimination会检测并删除不会被执行的代码和无用的变量。在所有PASS中lower会根据编写好的优化规则将SSA中间表示从与体系结构(如X86、ARM等)无关的转换为体系结构相关的,这是通过添加大量编译规则实现的,是本文的主要关注点。
1. 浮点变量比较场景
浮点数在应用开发中有广泛的应用,如用来表示一个带小数的金额或积分,经常会出现浮点数与0比较的情况,如向数据库录入一个商品时,为防止商品信息错误,可以检测录入的金额是否大于0,当用户购买产品时,可能需要先做一个验证,检测账户上金额是否大于0,如果满足再去查询商品信息、录入订单等,这样可以在交易的开始阶段排除一些无效或恶意的请求。
很多直播网站会举行年度活动,通过榜单展现用户活动期间累计送出礼物的金额,排名靠前的用户会登上榜单。经常用浮点数表示累计金额,活动刚开始时,需要屏蔽掉积分小于等于0的条目,可能会用到如下函数:
func comp(x float64, arr []int) {
for i := 0; i < len(arr); i++ {
if x > 0 {
arr[i] = 1
}
}
}
使用Go compile工具查看该函数的汇编代码(为便于理解,省略了部分无用代码):
go tool compile -S main.go
"".comp STEXT size=80 args=0x20 locals=0x0 leaf
0x0000 00000 (main.go:3) TEXT "".comp(SB), LEAF|NOFRAME|ABIInternal, $0-32
#-------------------------将栈上数据取到寄存器中------------------------------
..................................
0x0000 00000 (main.go:4) MOVD "".arr+16(FP), R0 // 取数组arr长度信息到寄存器R0中
..................................
0x0004 00004 (main.go:4) MOVD "".arr+8(FP), R1 // 取数组arr地址值到寄存器R1中
0x0008 00008 (main.go:4) FMOVD "".x(FP), F0 // 将参数x放入F0寄存器
0x000c 00012 (main.go:4) MOVD ZR, R2 // ZR表示0,此处R2 清零
#---------------------------for循环执行逻辑----------------------------------
0x0010 00016 (main.go:4) JMP 24 // 第一轮循环直接跳到条件比较 不增加i
0x0014 00020 (main.go:4) ADD $1, R2, R2 // i++
0x0018 00024 (main.go:4) CMP R0, R2 // i < len(arr) 比较
0x001c 00028 (main.go:4) BGE 68 // i == len(arr) 跳转到末尾
#--------if x > 0---------
0x0020 00032 (main.go:5) FMOVD ZR, F1 // 将0复制到浮点寄存器F1
0x0024 00036