导读|Go 的函数调用时参数是通过栈传递还是寄存器传递?应用哪个版本的 Go 语言能让程序运行性能晋升 5%?腾讯后盾开发工程师涂明光将带你由浅入深理解函数调用,并联合不同版本 Go 进行实操解答。
函数调用基本概念
1)调用者 caller 与被调用者 callee
如果一个函数调用另外一个函数,那么该函数被称为调用者函数,也叫做caller,而被调用的函数称为被调用者函数,也叫做callee。比方函数 main 中调用 sum 函数,那么 main 就是 caller,而 sum 函数就是 callee。
2)函数栈和函数栈帧
函数执行时须要有足够的内存空间,供它寄存局部变量、参数等数据,这段空间对应到虚拟地址空间的栈,也即函数栈。在古代支流机器架构上(例如 x86)中,栈都是向下成长的。栈的增长方向是从高位地址到位置地址向下进行增长。
调配给一个个函数的栈空间被称为“函数栈帧”。Go 语言中函数栈帧布局是这样的:先是调用者 caller 栈基地址,而后是调用者函数 caller 的局部变量、接着是被调用函数 callee 的返回值和参数。而后是被调用者 callee 的栈帧。
留神,栈和栈帧是不一样的。在一个函数调用链中,比方函数 A 调用 B,B 调用 C,则在函数栈上,A 的栈帧在下面,上面顺次是 B、C 的函数栈帧。Go1.17 以前的版本,函数栈空间布局如下:
函数调用剖析
通过在 centos8 上装置 gvm,能够不便切换多个 Go 版本测试不同版本的个性。
gvm 地址:https://github.com/moovweb/gvm
执行:
gvm list
显示 gvm 装置的 go 版本列表:
go1.14.2
go1.15.14
go1.15.7
go1.16.1
go1.16.13
go1.17.1
go1.18
go1.18.1
system
1)Go15 版本函数调用剖析
执行
gvm use go1.15.14
切换到 go1.15.14 版本,咱们定义一个函数调用:
package main
func main() {
var r1, r2, r3, r4, r5, r6, r7 int64 = 1, 2, 3, 4, 5, 6, 7
A(r1, r2, r3, r4, r5, r6, r7)
}
func A(p1, p2, p3, p4, p5, p6, p7 int64) int64 {return p1 + p2 + p3 + p4 + p5 + p6 + p7}
应用命令打印出 main.go 汇编:
GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go
接下来咱们剖析 main 函数的汇编代码:
"".main STEXT size=190 args=0x0 locals=0x80
0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $128-0 #main 函数定义, $128-0:128 示意将调配的 main 函数的栈帧大小;0 指定了调用方传入的参数,因为 main 是最上层函数,这里没有入参
0x0000 00000 (main.go:3) MOVQ (TLS), CX # 将本地线程存储信息保留到 CX 寄存器中
0x0009 00009 (main.go:3) CMPQ SP, 16(CX) # 栈溢出检测:比拟以后栈顶地址 (SP 寄存器寄存的) 与本地线程存储的栈顶地址
0x000d 00013 (main.go:3) PCDATA $0, $-2 # PCDATA,FUNCDATA 用于 Go 汇编额定信息,不用关注
0x000d 00013 (main.go:3) JLS 180 # 如果以后栈顶地址 (SP 寄存器寄存的) 小于本地线程存储的栈顶地址,则跳到 180 处代码处进行栈决裂扩容操作
0x0013 00019 (main.go:3) PCDATA $0, $-1
0x0013 00019 (main.go:3) ADDQ $-128, SP # 为 main 函数栈帧调配了 128 字节的空间,留神此时的 SP 寄存器指向,会往下挪动 128 个字节
0x0017 00023 (main.go:3) MOVQ BP, 120(SP) # BP 寄存器寄存的是 main 函数 caller 的基址,movq 这条指令是将 main 函数 caller 的基址入栈。0x001c 00028 (main.go:3) LEAQ 120(SP), BP # 将 main 函数的基址寄存到到 BP 寄存器
0x0021 00033 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0021 00033 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0021 00033 (main.go:4) MOVQ $1, "".r1+112(SP) # main 函数局部变量 r1 入栈
0x002a 00042 (main.go:4) MOVQ $2, "".r2+104(SP) # main 函数局部变量 r2 入栈
0x0033 00051 (main.go:4) MOVQ $3, "".r3+96(SP) # main 函数局部变量 r3 入栈
0x003c 00060 (main.go:4) MOVQ $4, "".r4+88(SP) # main 函数局部变量 r4 入栈
0x0045 00069 (main.go:4) MOVQ $5, "".r5+80(SP) # main 函数局部变量 r5 入栈
0x004e 00078 (main.go:4) MOVQ $6, "".r6+72(SP) # main 函数局部变量 r6 入栈
0x0057 00087 (main.go:4) MOVQ $7, "".r7+64(SP) # main 函数局部变量 r7 入栈
0x0060 00096 (main.go:5) MOVQ "".r1+112(SP), AX # 将局部变量 r1 传给寄存器 AX
0x0065 00101 (main.go:5) MOVQ AX, (SP) # 寄存器 AX 将局部变量 r1 退出栈头 SP 指向的地位
0x0069 00105 (main.go:5) MOVQ "".r2+104(SP), AX # 将局部变量 r2 传给寄存器 AX
0x006e 00110 (main.go:5) MOVQ AX, 8(SP) # 寄存器 AX 将局部变量 r2 退出栈头 SP+ 8 指向的地位
0x0073 00115 (main.go:5) MOVQ "".r3+96(SP), AX # 将局部变量 r3 传给寄存器 AX
0x0078 00120 (main.go:5) MOVQ AX, 16(SP) # 寄存器 AX 将局部变量 r3 退出栈头 SP+16 指向的地位
0x007d 00125 (main.go:5) MOVQ "".r4+88(SP), AX # 将局部变量 r4 传给寄存器 AX
0x0082 00130 (main.go:5) MOVQ AX, 24(SP) # 寄存器 AX 将局部变量 r4 退出栈头 SP+24 指向的地位
0x0087 00135 (main.go:5) MOVQ "".r5+80(SP), AX # 将局部变量 r5 传给寄存器 AX
0x008c 00140 (main.go:5) MOVQ AX, 32(SP) # 寄存器 AX 将局部变量 r4 退出栈头 SP+32 指向的地位
0x0091 00145 (main.go:5) MOVQ "".r6+72(SP), AX # 将局部变量 r6 传给寄存器 AX
0x0096 00150 (main.go:5) MOVQ AX, 40(SP) # 寄存器 AX 将局部变量 r6 退出栈头 SP+40 指向的地位
0x009b 00155 (main.go:5) MOVQ "".r7+64(SP), AX # 将局部变量 r7 传给寄存器 AX
0x00a0 00160 (main.go:5) MOVQ AX, 48(SP) # 寄存器 AX 将局部变量 r7 退出栈头 SP+48 指向的地位
0x00a5 00165 (main.go:5) PCDATA $1, $0
0x00a5 00165 (main.go:5) CALL "".A(SB) # 调用 A 函数
0x00aa 00170 (main.go:6) MOVQ 120(SP), BP # 将栈上存储的 main 函数的调用方的基地址复原到 BP
0x00af 00175 (main.go:6) SUBQ $-128, SP # 减少 SP 的值,栈膨胀,发出调配给 main 函数栈帧的 128 字节空间
0x00b3 00179 (main.go:6) RET
从汇编代码的正文中,咱们能够分明的看到,main 函数调用 A 函数的局部变量、入参在栈中的存储地位。main 函数通过 ADDQ $-128, SP 指令,一共在栈上调配了 128 字节的内存空间:
SP+64 ~ SP+112 指向的 56 个栈空间,存储的是 r1 ~ r7 这 7 个 main 函数的局部变量;SP+56 该地址接管函数 A 的返回值;SP~SP+48 指向的 56 个字节空间,用来寄存 A 函数的 7 个入参。
综上,在 Go1.15.14 版本的函数调用中:参数齐全通过栈传递;参数列表从右至左顺次压栈。当程序筹备好函数的入参之后,会调用汇编指令 CALL “”.A(SB),这个指令首先会将 main 的返回地址 (8 bytes) 存入栈中,而后扭转以后的栈指针 SP 并执行 A 函数的汇编指令。栈空间变为:
上面剖析 A 函数:
"".A STEXT nosplit size=50 args=0x40 locals=0x0
0x0000 00000 (main.go:8) TEXT "".A(SB), NOSPLIT|ABIInternal, $0-64 #A 函数定义, $0-64:0 示意将调配的 A 函数的栈帧大小;64 指定了调用方传入的参数和函数的返回值的大小,入参 7 个,返回值 1 个,都是 8 字节,共 64 字节
0x0000 00000 (main.go:8) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:8) MOVQ $0, "".~r7+64(SP) # 这里 SP+64 就是下面 main 栈空间中用来接管返回值的地址
0x0009 00009 (main.go:9) MOVQ "".p1+8(SP), AX # A 返回值和 r1 参数求和后,放入 AX 寄存器
0x000e 00014 (main.go:9) ADDQ "".p2+16(SP), AX # AX 寄存器的值再和 r2 参数求和,后果放入 AX
0x0013 00019 (main.go:9) ADDQ "".p3+24(SP), AX # AX 寄存器的值再和 r3 参数求和,后果放入 AX
0x0018 00024 (main.go:9) ADDQ "".p4+32(SP), AX # AX 寄存器的值再和 r4 参数求和,后果放入 AX
0x001d 00029 (main.go:9) ADDQ "".p5+40(SP), AX # AX 寄存器的值再和 r5 参数求和,后果放入 AX
0x0022 00034 (main.go:9) ADDQ "".p6+48(SP), AX # AX 寄存器的值再和 r6 参数求和,后果放入 AX
0x0027 00039 (main.go:9) ADDQ "".p7+56(SP), AX # AX 寄存器的值再和 r7 参数求和,后果放入 AX
0x002c 00044 (main.go:9) MOVQ AX, "".~r7+64(SP) # AX 寄存器的值 写回 main 栈空间中用来接管返回值的地址 SP+64 中
0x0031 00049 (main.go:9) RET
须要留神的是,””.~r7+64(SP)是上图中,main 函数用来接管 A 函数返回值的地址 SP+56,因为 CALL “”.A(SB)将 main 返回地址压栈后,SP 向下挪动了 8 字节。
从 A 函数的汇编剖析,能够失去论断:Go1.17.1 之前版本,callee 函数返回值通过 caller 栈传递;如果咱们让 main 接管 A 函数的返回值,会发现 callee 的返回值也是通过 caller 的栈空间传递。
2)Go17 版本函数调用剖析
执行
gvm use go1.17.1
切换到 go1.17.1 版本,批改 main.go 代码构造如下:
package main
func main() {
var r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11 int64 = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
a, b := A(r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11)
c := a + b
print(c)
}
func A(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 int64) (int64, int64) {return p1 + p2 + p3 + p4 + p5 + p6 + p7, p2 + p4 + p6 + p7 + p8 + p9 + p10 + p11}
应用命令打印出 main.go 汇编:
GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go
剖析 main 函数的汇编代码:
"".main STEXT size=362 args=0x0 locals=0xe0 funcid=0x0
0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $224-0 #main 函数定义, $224-0:224 示意将调配的 main 函数的栈帧大小;0 指定了调用方传入的参数,因为 main 是最上层函数,这里没有入参
0x0000 00000 (main.go:3) LEAQ -96(SP), R12
0x0005 00005 (main.go:3) CMPQ R12, 16(R14)
0x0009 00009 (main.go:3) PCDATA $0, $-2
0x0009 00009 (main.go:3) JLS 349
0x000f 00015 (main.go:3) PCDATA $0, $-1
0x000f 00015 (main.go:3) SUBQ $224, SP # 为 main 函数栈帧调配了 224 字节的空间,留神此时的 SP 寄存器指向,会往下挪动 224 个字节
0x0016 00022 (main.go:3) MOVQ BP, 216(SP) # BP 寄存器寄存的是 main 函数 caller 的基址,movq 这条指令是将 main 函数 caller 的基址入栈
0x001e 00030 (main.go:3) LEAQ 216(SP), BP # 将 main 函数的基址寄存到到 BP 寄存器
0x0026 00038 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0026 00038 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0026 00038 (main.go:4) MOVQ $1, "".r1+168(SP) # main 函数局部变量 r1 入栈
0x0032 00050 (main.go:4) MOVQ $2, "".r2+144(SP) # main 函数局部变量 r2 入栈
0x003e 00062 (main.go:4) MOVQ $3, "".r3+136(SP) # main 函数局部变量 r3 入栈
0x004a 00074 (main.go:4) MOVQ $4, "".r4+128(SP) # main 函数局部变量 r4 入栈
0x0056 00086 (main.go:4) MOVQ $5, "".r5+120(SP) # main 函数局部变量 r5 入栈
0x005f 00095 (main.go:4) MOVQ $6, "".r6+112(SP) # main 函数局部变量 r6 入栈
0x0068 00104 (main.go:4) MOVQ $7, "".r7+104(SP) # main 函数局部变量 r7 入栈
0x0071 00113 (main.go:4) MOVQ $8, "".r8+96(SP) # main 函数局部变量 r8 入栈
0x007a 00122 (main.go:4) MOVQ $9, "".r9+88(SP) # main 函数局部变量 r9 入栈
0x0083 00131 (main.go:4) MOVQ $10, "".r10+160(SP) # main 函数局部变量 r10 入栈
0x008f 00143 (main.go:4) MOVQ $11, "".r11+152(SP) # main 函数局部变量 r11 入栈
0x009b 00155 (main.go:5) MOVQ "".r2+144(SP), BX # 将局部变量 r2 传给寄存器 BX
0x00a3 00163 (main.go:5) MOVQ "".r3+136(SP), CX # 将局部变量 r3 传给寄存器 CX
0x00ab 00171 (main.go:5) MOVQ "".r4+128(SP), DI # 将局部变量 r4 传给寄存器 DI
0x00b3 00179 (main.go:5) MOVQ "".r5+120(SP), SI # 将局部变量 r5 传给寄存器 SI
0x00b8 00184 (main.go:5) MOVQ "".r6+112(SP), R8 # 将局部变量 r6 传给寄存器 R8
0x00bd 00189 (main.go:5) MOVQ "".r7+104(SP), R9 # 将局部变量 r7 传给寄存器 R9
0x00c2 00194 (main.go:5) MOVQ "".r8+96(SP), R10 # 将局部变量 r8 传给寄存器 R10
0x00c7 00199 (main.go:5) MOVQ "".r9+88(SP), R11 # 将局部变量 r9 传给寄存器 R11
0x00cc 00204 (main.go:5) MOVQ "".r10+160(SP), DX # 将局部变量 r10 传给寄存器 DX
0x00d4 00212 (main.go:5) MOVQ "".r1+168(SP), AX # 将局部变量 r1 传给寄存器 DX
0x00dc 00220 (main.go:5) MOVQ DX, (SP) # 将寄存器 DX 保留的 r10 传给 SP 指向的栈顶
0x00e0 00224 (main.go:5) MOVQ $11, 8(SP) # 将变量 r11 传给 SP+8
0x00e9 00233 (main.go:5) PCDATA $1, $0
0x00e9 00233 (main.go:5) CALL "".A(SB) # 调用 A 函数
0x00ee 00238 (main.go:5) MOVQ AX, ""..autotmp_14+208(SP) # 将寄存器 AX 存的函数 A 的第一个返回值 a 赋值给 SP+208
0x00f6 00246 (main.go:5) MOVQ BX, ""..autotmp_15+200(SP) # 将寄存器 BX 存的函数 A 的第二个返回值 b 赋值给 SP+200
0x00fe 00254 (main.go:5) MOVQ ""..autotmp_14+208(SP), DX # 将 SP+208 保留的 A 函数第一个返回值 a 传给寄存器 DX
0x0106 00262 (main.go:5) MOVQ DX, "".a+192(SP) # 将 A 函数第一个返回值 a 通过寄存器 DX 入栈到 SP+192
0x010e 00270 (main.go:5) MOVQ ""..autotmp_15+200(SP), DX # 将 SP+200 保留的 A 函数第二个返回值 b 传给寄存器 DX
0x0116 00278 (main.go:5) MOVQ DX, "".b+184(SP) # 将第二个返回值 b 通过寄存器 DX 入栈到 SP+184
0x011e 00286 (main.go:6) MOVQ "".a+192(SP), DX # 将返回值 a 传给 DX 寄存器
0x0126 00294 (main.go:6) ADDQ "".b+184(SP), DX # 将 a + b 赋值给 DX 寄存器
0x012e 00302 (main.go:6) MOVQ DX, "".c+176(SP) # 将 DX 寄存器的值入栈到 SP+176
0x0136 00310 (main.go:7) CALL runtime.printlock(SB)
0x013b 00315 (main.go:7) MOVQ "".c+176(SP), AX # 将 SP+176 存储的入参 c 赋值给 AX
0x0143 00323 (main.go:7) CALL runtime.printint(SB) # 调用打印函数打印 c
0x0148 00328 (main.go:7) CALL runtime.printunlock(SB)
0x014d 00333 (main.go:8) MOVQ 216(SP), BP
0x0155 00341 (main.go:8) ADDQ $224, SP
0x015c 00348 (main.go:8) RET
通过下面汇编代码的正文,咱们能够看到:main 函数调用 A 函数的参数个数为 11 个,其中前 9 个参数别离是通过寄存器 AX、BX、CX、DI、SI、R8、R9、R10、R11 传递,前面两个通过栈顶的 SP,SP+ 8 地址传递。
上面看 A 函数在 Go1.17.1 的汇编代码:
"".A STEXT nosplit size=175 args=0x58 locals=0x18 funcid=0x0
0x0000 00000 (main.go:10) TEXT "".A(SB), NOSPLIT|ABIInternal, $24-88
0x0000 00000 (main.go:10) SUBQ $24, SP # 为 A 函数栈帧调配了 24 字节的空间
0x0004 00004 (main.go:10) MOVQ BP, 16(SP)
0x0009 00009 (main.go:10) LEAQ 16(SP), BP
0x000e 00014 (main.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:10) FUNCDATA $5, "".A.arginfo1(SB)
0x000e 00014 (main.go:10) MOVQ AX, "".p1+48(SP) # 寄存器 AX 存储的 r1 赋值给 SP+48
0x0013 00019 (main.go:10) MOVQ BX, "".p2+56(SP) # 寄存器 BX 存储的 r2 赋值给 SP+56
0x0018 00024 (main.go:10) MOVQ CX, "".p3+64(SP) # 寄存器 CX 存储的 r3 赋值给 SP+64
0x001d 00029 (main.go:10) MOVQ DI, "".p4+72(SP) # 寄存器 DI 存储的 r4 赋值给 SP+72
0x0022 00034 (main.go:10) MOVQ SI, "".p5+80(SP) # 寄存器 SI 存储的 r5 赋值给 SP+80
0x0027 00039 (main.go:10) MOVQ R8, "".p6+88(SP) # 寄存器 R8 存储的 r6 赋值给 SP+88
0x002c 00044 (main.go:10) MOVQ R9, "".p7+96(SP) # 寄存器 R9 存储的 r7 赋值给 SP+96
0x0031 00049 (main.go:10) MOVQ R10, "".p8+104(SP) # 寄存器 R10 存储的 r8 赋值给 SP+104
0x0036 00054 (main.go:10) MOVQ R11, "".p9+112(SP) # 寄存器 R11 存储的 r9 赋值给 SP+112
0x003b 00059 (main.go:10) MOVQ $0, "".~r11+8(SP) # 初始化第一个返回值 a 寄存地址 SP+ 8 为 0
0x0044 00068 (main.go:10) MOVQ $0, "".~r12(SP) # 初始化第二个返回值 b 寄存地址 SP 为 0
0x004c 00076 (main.go:11) MOVQ "".p1+48(SP), CX # SP+48 存储的 r1 赋值给 CX 寄存器
0x0051 00081 (main.go:11) ADDQ "".p2+56(SP), CX # CX+r2 赋值给 CX 寄存器
0x0056 00086 (main.go:11) ADDQ "".p3+64(SP), CX # CX+r3 赋值给 CX 寄存器
0x005b 00091 (main.go:11) ADDQ "".p4+72(SP), CX # CX+r4 赋值给 CX 寄存器
0x0060 00096 (main.go:11) ADDQ "".p5+80(SP), CX # CX+r5 赋值给 CX 寄存器
0x0065 00101 (main.go:11) ADDQ "".p6+88(SP), CX # CX+r6 赋值给 CX 寄存器
0x006a 00106 (main.go:11) ADDQ "".p7+96(SP), CX # CX+r7 赋值给 CX 寄存器
0x006f 00111 (main.go:11) MOVQ CX, "".~r11+8(SP) # CX 寄存器赋值给第一个返回值寄存地址 SP+8
0x0074 00116 (main.go:11) MOVQ "".p2+56(SP), BX # r2 赋值给 BX 寄存器
0x0079 00121 (main.go:11) ADDQ "".p4+72(SP), BX # BX+r4 赋值给 BX 寄存器
0x007e 00126 (main.go:11) ADDQ "".p6+88(SP), BX # BX+r6 赋值给 BX 寄存器
0x0083 00131 (main.go:11) ADDQ "".p7+96(SP), BX # BX+r7 赋值给 BX 寄存器
0x0088 00136 (main.go:11) ADDQ "".p8+104(SP), BX # BX+r8 赋值给 BX 寄存器
0x008d 00141 (main.go:11) ADDQ "".p9+112(SP), BX # BX+r9 赋值给 BX 寄存器
0x0092 00146 (main.go:11) ADDQ "".p10+32(SP), BX # BX+r11 赋值给 BX 寄存器
0x0097 00151 (main.go:11) ADDQ "".p11+40(SP), BX # BX+r10 赋值给 BX 寄存器
0x009c 00156 (main.go:11) MOVQ BX, "".~r12(SP) # BX 寄存器赋值给第二个返回值寄存地址 SP
0x00a0 00160 (main.go:11) MOVQ "".~r11+8(SP), AX # 第一个返回值 SP+ 8 的值赋值给 AX 寄存器
0x00a5 00165 (main.go:11) MOVQ 16(SP), BP # main 返回地址赋值给 BP
0x00aa 00170 (main.go:11) ADDQ $24, SP # 回收 A 函数栈帧空间
0x00ae 00174 (main.go:11) RET
在 A 函数栈中,咱们能够看到:程序先把 r1 ~ r9 参数别离从寄存器赋值到 main 栈帧的入参地址局部,即以后的 SP+48~SP+112 位。其实这跟 GO1.15.14 的函数调用参数传递过程差不多,只不过一个是在 caller 中做参数从寄存器拷贝到栈上,一个是在 callee 中做参数从寄存器拷贝到栈上。而且前者只应用了 AX 一个寄存器,后者应用了 9 个不同的寄存器。
很多开发者看到这里,预计会有一个疑难:Go1.15 与 Go1.17 在寄存器拜访次数上和栈拜访次数上,没有区别。只是寄存器上的参数拷贝到栈上的产生机会不同?那么为什么 Go1.17 会有较高的性能劣势?
咱们把打印汇编的命令 GOOS=linux GOARCH=amd64 go tool compile -S -N -L main.go 中的 -N - L 禁用内联优化去掉(这才是性能比照的状态),咱们再看,会发现 Go17 的 A 函数会间接执行寄存器之间的加法,Go15 版本的 A 函数不会。
对 2.1 节程序执行命令:
GOOS=linux GOARCH=amd64 go tool compile -S main.go
Go1.15 优化后的汇编代码是:
"".A STEXT nosplit size=59 args=0x40 locals=0x0
0x0000 00000 (main.go:8) TEXT "".A(SB), NOSPLIT|ABIInternal, $0-64
0x0000 00000 (main.go:8) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:9) MOVQ "".p1+8(SP), AX #参数从栈赋值到寄存器 AX
0x0005 00005 (main.go:9) MOVQ "".p2+16(SP), CX #参数从栈赋值到寄存器 CX
0x000a 00010 (main.go:9) ADDQ CX, AX
0x000d 00013 (main.go:9) MOVQ "".p3+24(SP), CX #参数从栈赋值到寄存器 CX
0x0012 00018 (main.go:9) ADDQ CX, AX
0x0015 00021 (main.go:9) MOVQ "".p4+32(SP), CX
0x001a 00026 (main.go:9) ADDQ CX, AX
0x001d 00029 (main.go:9) MOVQ "".p5+40(SP), CX
0x0022 00034 (main.go:9) ADDQ CX, AX
0x0025 00037 (main.go:9) MOVQ "".p6+48(SP), CX
0x002a 00042 (main.go:9) ADDQ CX, AX
0x002d 00045 (main.go:9) MOVQ "".p7+56(SP), CX
0x0032 00050 (main.go:9) ADDQ CX, AX
0x0035 00053 (main.go:9) MOVQ AX, "".~r7+64(SP)
0x003a 00058 (main.go:9) RET
gvm 切换到 Go1.17.1 版本。对 2.1 节程序执行命令:
GOOS=linux GOARCH=amd64 go tool compile -S main.go
Go1.17 优化后的汇编代码是:
"".A STEXT nosplit size=21 args=0x38 locals=0x0 funcid=0x0
0x0000 00000 (main.go:8) TEXT "".A(SB), NOSPLIT|ABIInternal, $0-56
0x0000 00000 (main.go:8) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:8) FUNCDATA $5, "".A.arginfo1(SB)
0x0000 00000 (main.go:9) LEAQ (BX)(AX*1), DX
0x0004 00004 (main.go:9) ADDQ DX, CX #间接在寄存器之间做加法
0x0007 00007 (main.go:9) ADDQ DI, CX #间接在寄存器之间做加法
0x000a 00010 (main.go:9) ADDQ SI, CX
0x000d 00013 (main.go:9) ADDQ R8, CX
0x0010 00016 (main.go:9) LEAQ (R9)(CX*1), AX
0x0014 00020 (main.go:9) RET
比照发现:寄存器传参和栈传参,在编译器理论优化后的执行代码中,前者间接会在寄存器之间做加法,后者多了从栈拷贝数据到寄存器到动作,因而前者效率更高。
通过剖析 Go1.17.1 函数调用过程,咱们发现:
参数传递应用了多个寄存器,并且被调用方 callee 的返回值由 callee 自身的栈帧负责寄存,而不是放在 caller 的栈帧上;当 callee 的栈帧被销毁时,其返回值通过 AX,BX 等寄存器传递给调用方 caller。
9 个以内的参数通过寄存器传递,9 个以外的通过栈传递。如果将 A 函数的返回值个数设置大于 9 个,同样会发现,9 个以内的返回值通过寄存器传递,9 个以外的通过栈传递。
为何高版本 Go 要改用寄存器传参?
至于为什么 Go1.17.1 函数调用的参数传递开始基于寄存器进行传递,起因无外乎。
第一,CPU 拜访寄存器比拜访栈要快的多。函数调用通过寄存器传参比栈传参,性能要高 5%。
第二,晚期 Go 版本为了升高实现的复杂度,对立应用栈传递参数和返回值,不惜牺牲函数调用的性能。
第三,Go 从 1.17.1 版本,开始反对 多 ABI(application binary interface 应用程序二进制接口,规定了程序在机器层面的操作标准,次要包含调用规约 calling convention),次要是两个 ABI:一个是老版本 Go 采纳的平台通用 ABI0,一个是 Go 独特的 ABIInternal,前者遵循平台通用的函数调用约定,实现简略,不必放心底层 cpu 架构寄存器的差别;后者能够指定特定的函数调用标准,能够针对特定性能瓶颈进行优化,在多个 Go 版本之间能够迭代,灵活性强,反对寄存器传参晋升性能。
所谓“调用规约(calling convention)”是调用方和被调用方对于函数调用的一个明确的约定,包含:函数参数与返回值的传递形式、传递程序。只有单方都恪守同样的约定,函数能力被正确地调用和执行。如果不恪守这个约定,函数将无奈正确执行。
总结
综合下面的剖析,咱们得出结论:
Go1.17.1 之前的函数调用,参数都在栈上传递;Go1.17.1 当前,9 个以内的参数在寄存器传递,9 个以外的在栈上传递;Go1.17.1 之前版本,callee 函数返回值通过 caller 栈传递;Go1.17.1 当前,函数调用的返回值,9 个以内通过寄存器传递回 caller,9 个以外在栈上传递。
在 Go 1.17 的版本公布阐明文档中有提到:切换到基于寄存器的调用常规后,一组有代表性的 Go 包和程序的基准测试显示,Go 程序的运行性能进步了约 5%,二进制文件大小缩小约 2%。
因为 CPU 拜访寄存器的速度要远高于栈内存,参数在栈上传递会减少栈内存空间,并且影响栈的扩缩容和垃圾回收,改为寄存器传递,这些毛病都失去了优化,Go 程序在从低版本升级到 17 版本后,性能有肯定的晋升。在业务容许的状况下,这里倡议各位开发者能够把本人程序的 Go 版本升级到 17 及以上。
公众号后盾回复“GO117”取得作者举荐 GO 相干作品
腾讯工程师技术干货中转:
1、H5 开屏从龟速到闪电,企微是如何做到的
2、全网首次揭秘:微秒级“复活”网络的 HARP 协定及其关键技术
3、闰秒终于要勾销了!一文详解其起源及影响
4、万字避坑指南!C++ 的缺点与思考(下)
浏览原文