原文链接:面试官:来说一说 Go 语言的函数调用常规
前言
哈喽,大家好,我是
asong
。良久没有更新了,最近因工作须要忙着写python
,Go
语言我都有些陌生了,然而我不能放弃Go
语言,该学习还是要学习的,明天与大家聊一聊Go
语言的函数调用常规,调用常规是调用方和被调用方对于参数和返回值传递的约定,Go 语言的调用常规在 1.17 版本进行了优化,本文咱们就看一下两个版本的调用常规是什么样的吧~。
1.17 版本前栈传递
在 Go1.17
版本之前,Go
语言函数调用是通过栈来传递的,咱们应用 Go1.12
版本写个例子来看一下:
package main
func Test(a, b int) (int, int) {return a + b, a - b}
func main() {Test(10, 20)
}
执行 go tool compile -S -N -l main.go
能够看到其汇编指令,咱们分两局部来看,先看 main
函数局部:
"".main STEXT size=68 args=0x0 locals=0x28
0x0000 00000 (main.go:7) TEXT "".main(SB), ABIInternal, $40-0
0x0000 00000 (main.go:7) MOVQ (TLS), CX
0x0009 00009 (main.go:7) CMPQ SP, 16(CX)
0x000d 00013 (main.go:7) JLS 61
0x000f 00015 (main.go:7) SUBQ $40, SP // 调配 40 字节栈空间
0x0013 00019 (main.go:7) MOVQ BP, 32(SP) // 基址指针存储到栈上
0x0018 00024 (main.go:7) LEAQ 32(SP), BP
0x001d 00029 (main.go:7) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:7) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:7) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:8) PCDATA $2, $0
0x001d 00029 (main.go:8) PCDATA $0, $0
0x001d 00029 (main.go:8) MOVQ $10, (SP) // 第一个参数压栈
0x0025 00037 (main.go:8) MOVQ $20, 8(SP) // 第二个参数压栈
0x002e 00046 (main.go:8) CALL "".Test(SB) // 调用函数 Test
0x0033 00051 (main.go:9) MOVQ 32(SP), BP // Test 函数返回后复原栈基址指针
0x0038 00056 (main.go:9) ADDQ $40, SP // 销毁 40 字节栈内存
0x003c 00060 (main.go:9) RET // 返回
0x003d 00061 (main.go:9) NOP
0x003d 00061 (main.go:7) PCDATA $0, $-1
0x003d 00061 (main.go:7) PCDATA $2, $-1
0x003d 00061 (main.go:7) CALL runtime.morestack_noctxt(SB)
0x0042 00066 (main.go:7) JMP 0
0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2e 48 eH..%....H;a.v.H
0x0010 83 ec 28 48 89 6c 24 20 48 8d 6c 24 20 48 c7 04 ..(H.l$ H.l$ H..
0x0020 24 0a 00 00 00 48 c7 44 24 08 14 00 00 00 e8 00 $....H.D$.......
0x0030 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 e8 00 00 ...H.l$ H..(....
0x0040 00 00 eb bc ....
rel 5+4 t=16 TLS+0
rel 47+4 t=8 "".Test+0
rel 62+4 t=8 runtime.morestack_noctxt+0
通过下面的汇编指令咱们能够剖析出,参数 10
、20
依照从右向左进行压栈,所以第一个参数在栈顶的地位 SP~SP+8
,第二个参数存储在SP+8 ~ SP+16
,参数筹备结束后就去调用TEST
函数,对应的汇编指令:CALL "".Test(SB)
,对应的汇编指令如下:
"".Test STEXT nosplit size=49 args=0x20 locals=0x0
0x0000 00000 (main.go:3) TEXT "".Test(SB), NOSPLIT|ABIInternal, $0-32
0x0000 00000 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:3) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (main.go:3) PCDATA $2, $0
0x0000 00000 (main.go:3) PCDATA $0, $0
0x0000 00000 (main.go:3) MOVQ $0, "".~r2+24(SP)// SP+16 ~ SP+24 存储第一个返回值
0x0009 00009 (main.go:3) MOVQ $0, "".~r3+32(SP)
// SP+24 ~ SP+32 存储第二个返回值
0x0012 00018 (main.go:4) MOVQ "".a+8(SP), AX // 第一个参数放入 AX 寄存器 AX = 10
0x0017 00023 (main.go:4) ADDQ "".b+16(SP), AX // 第二个参数放入 AX 寄存器做加法 AX = AX + 20 = 30
0x001c 00028 (main.go:4) MOVQ AX, "".~r2+24(SP)
// AX 寄存器中的值在存回栈中:24(SP)
0x0021 00033 (main.go:4) MOVQ "".a+8(SP), AX
// 第一个参数放入 AX 寄存器 AX = 10
0x0026 00038 (main.go:4) SUBQ "".b+16(SP), AX
// 第二个参数放入 AX 寄存器做减法 AX = AX - 20 = -10
0x002b 00043 (main.go:4) MOVQ AX, "".~r3+32(SP)
// AX 寄存器中的值在存回栈中:32(SP)
0x0030 00048 (main.go:4) RET // 函数返回
通过以上的汇编指令咱们能够得出结论:Go
语言应用栈传递参数和接管返回值,多个返回值也是通过多调配一些内存来实现的。
这种基于栈传递参数和接管返回值的设计大大降低了实现的复杂度,然而就义了函数调用的性能,像 C
语言采纳同时应用栈和寄存器传递参数,在性能上是优于 Go
语言的,上面咱们就来看一看 Go1.17
引入的寄存器传参。
为什么寄存器传参性能优于栈传参
咱们都晓得 CPU
是一台计算机的运算外围和管制外围,其次要性能是解释计算机指令以及解决计算机软件中的数据,CPU
的大抵内部结构如下:
次要由运算器和控制器组成,运算器负责实现算术运算和逻辑运算,寄存器长期保留将要被运算器解决的数据和解决后的后果,回到主题上,寄存器是 CPU
外部组件,而存储个别在内部,CPU
操作寄存器与读取内存的速度差距是数量级别的,当要进行数据计算时,如果数据处于内存中,CPU
须要先将数据从内存拷贝到寄存器进行计算,所以对于栈传递参数与接管返回值这种调用规约,每次计算都须要从内存拷贝到寄存器,计算结束在拷贝回内存,如果应用寄存器传参的话,参数就曾经按程序放在特定寄存器了,这样就缩小了内存和寄存器之间的数据拷贝,从而改善了性能,提供程序运行效率。
既然寄存器传参性能高于栈传递参数,为什么所有语言不都应用寄存器传递参数呢?因为不同架构上的寄存器差别不同,所以要反对寄存器传参就要在编译器上进行反对,这要就使编译器变得更加简单且不易保护,并且寄存器的数量也是无限的,还要思考超过寄存器数量的参数应该如何传递。
1.17 基于寄存器传递
Go
语言在 1.17
版本设计了一套基于寄存器传参的调用规约,目前也只反对 x86
平台,咱们也是通过一个简略的例子看一下:
func Test(a, b, c, d int) (int,int,int,int) {return a, b, c, d}
func main() {Test(1, 2, 3 ,4)
}
执行 go tool compile -S -N -l main.go
能够看到其汇编指令,咱们分两局部来看,先看 main
函数局部:
"".main STEXT size=62 args=0x0 locals=0x28 funcid=0x0
0x0000 00000 (main.go:7) TEXT "".main(SB), ABIInternal, $40-0
0x0000 00000 (main.go:7) CMPQ SP, 16(R14)
0x0004 00004 (main.go:7) PCDATA $0, $-2
0x0004 00004 (main.go:7) JLS 55
0x0006 00006 (main.go:7) PCDATA $0, $-1
0x0006 00006 (main.go:7) SUBQ $40, SP// 调配 40 字节栈空间,基址指针存储到栈上
0x000a 00010 (main.go:7) MOVQ BP, 32(SP)// 基址指针存储到栈上
0x000f 00015 (main.go:7) LEAQ 32(SP), BP
0x0014 00020 (main.go:7) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:7) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:8) MOVL $1, AX // 参数 1 应用 AX 寄存器传递
0x0019 00025 (main.go:8) MOVL $2, BX // 参数 2 应用 BX 寄存器传递
0x001e 00030 (main.go:8) MOVL $3, CX // 参数 3 应用 CX 寄存器传递
0x0023 00035 (main.go:8) MOVL $4, DI // 参数 4 应用 DI 寄存器传递
0x0028 00040 (main.go:8) PCDATA $1, $0
0x0028 00040 (main.go:8) CALL "".Test(SB) // 调用 Test 函数
0x002d 00045 (main.go:9) MOVQ 32(SP), BP // Test 函数返回后复原栈基址指针
0x0032 00050 (main.go:9) ADDQ $40, SP // 销毁 40 字节栈内存
0x0036 00054 (main.go:9) RET // 返回
通过下面的汇编指令咱们能够剖析出,当初参数曾经不是从右向左进行压栈了,参数间接在寄存器上了,参数筹备结束后就去调用 TEST
函数,对应的汇编指令:CALL "".Test(SB)
,对应的汇编指令如下:
"".Test STEXT nosplit size=133 args=0x20 locals=0x28 funcid=0x0
0x0000 00000 (main.go:3) TEXT "".Test(SB), NOSPLIT|ABIInternal, $40-32
0x0000 00000 (main.go:3) SUBQ $40, SP
0x0004 00004 (main.go:3) MOVQ BP, 32(SP)
0x0009 00009 (main.go:3) LEAQ 32(SP), BP
0x000e 00014 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (main.go:3) FUNCDATA $5, "".Test.arginfo1(SB)
0x000e 00014 (main.go:3) MOVQ AX, "".a+48(SP) // 从寄存器 AX 获取参数 1 放入栈中 48(SP)
0x0013 00019 (main.go:3) MOVQ BX, "".b+56(SP) // 从寄存器 BX 获取参数 2 放入栈中 56(SP)
0x0018 00024 (main.go:3) MOVQ CX, "".c+64(SP) // 从寄存器 CX 获取参数 3 放入栈中 64(SP)
0x001d 00029 (main.go:3) MOVQ DI, "".d+72(SP) // 从寄存器 DI 获取参数 4 放入栈中 72(SP)
0x0022 00034 (main.go:3) MOVQ $0, "".~r4+24(SP)
0x002b 00043 (main.go:3) MOVQ $0, "".~r5+16(SP)
0x0034 00052 (main.go:3) MOVQ $0, "".~r6+8(SP)
0x003d 00061 (main.go:3) MOVQ $0, "".~r7(SP)
0x0045 00069 (main.go:4) MOVQ "".a+48(SP), DX // 以下操作是返回值放到寄存器中返回
0x004a 00074 (main.go:4) MOVQ DX, "".~r4+24(SP)
0x004f 00079 (main.go:4) MOVQ "".b+56(SP), DX
0x0054 00084 (main.go:4) MOVQ DX, "".~r5+16(SP)
0x0059 00089 (main.go:4) MOVQ "".c+64(SP), DX
0x005e 00094 (main.go:4) MOVQ DX, "".~r6+8(SP)
0x0063 00099 (main.go:4) MOVQ "".d+72(SP), DI
0x0068 00104 (main.go:4) MOVQ DI, "".~r7(SP)
0x006c 00108 (main.go:4) MOVQ "".~r4+24(SP), AX
0x0071 00113 (main.go:4) MOVQ "".~r5+16(SP), BX
0x0076 00118 (main.go:4) MOVQ "".~r6+8(SP), CX
0x007b 00123 (main.go:4) MOVQ 32(SP), BP
0x0080 00128 (main.go:4) ADDQ $40, SP
0x0084 00132 (main.go:4) RET
传参和返回都采纳了寄存器进行传递,并且返回值和输出都应用了完全相同的寄存器序列,并且应用的程序也是统一的。
因为这个优化,在一些函数调用嵌套档次较深的场景下,内存有肯定概率会升高,有机会做压测能够试一试~。
总结
熟练掌握并了解函数的调用过程是咱们深刻学习 Go
语言的重要一课,看完本文心愿你曾经熟练掌握了函数的调用常规~。
好啦,本文到这里就完结了,我是asong,咱们下期见。
欢送关注公众号:Golang 梦工厂