0%

go语言函数传参分析

通过分析汇编得到的go1.17前后函数传参详情

在go1.17的release note中,有这样一句话:Go 1.17 implements a new way of passing function arguments and results using registers instead of the stack. 也就是说,在go1.17之前,函数传参的方式是通过栈来传递的,而在go1.17之后,函数传参的方式变成了通过寄存器来传递,这样做的好处是可以减少栈的使用,提高函数调用的效率。在这里我通过分析汇编,得到了更多go语言函数调用的细节,这里做一个简单的记录。

demo

在下面的例子中,我们定义了一个函数Func,它接受12个参数,返回11个返回值。然后我们在main函数中调用了Func函数。

1
2
3
4
5
6
7
8
9
package main

func Func(a, b, c, d, e, f, g, h, i, j, k, l int) (int, int, int, int, int, int, int, int, int, int, int) {
return a, b, c, d, e, f, g, h, i, j, k
}

func main() {
Func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
}

下面我们都用这个例子来说明。

1. go1.17之前的函数传参方式

在go1.17之前,函数不论是参数还是返回值,都是用栈。在调用函数时,首先在栈上为返回值留出空间,然后将参数依次压入栈中,最后调用函数(同时压入返回地址)。不论是参数还是返回值都是按照从右到左的顺序压入栈中。

对于上面的函数,可以得到如下的效果:
stack_1_16.drawio-1.png

2. go1.17之后的函数传参方式

在go1.17及之后,会将前九个参数和返回值放在寄存器里面,其余的参数和返回值放在栈里面。寄存器的使用顺序是rax, rbx, rcx, rdi, rsi, r8d, r9d, r10d, r11d,栈上的参数和返回值的顺序是从右到左的依次入栈的。

对于上面的函数,可以得到如下的效果:
stack1_20.drawio-1.png

go语言中的复杂类型是如何传递的

我还研究了一下go语言中的复杂类型是如何传递的,比如struct、slice等。

struct

对于struct类型,实际上是把struct做展开,对于struct中的每个元素依次通过寄存器或者栈传递。比如下面的例子,对于go1.17之前的一个结构体参数,在栈上有这样的效果:

struct16.drawio.png

slice

对于slice类型,实际上需要同时传递三个参数:指向slice的指针、slice的长度和slice的容量。对于go1.17之前的一个slice参数,在栈上有这样的效果:

slice-1-16.drawio.png

go1.17之后有相似的效果,只是可能会放在寄存器里面。

方法

对于方法,实际上是把方法的接收者作为第一个参数传递,然后再传递其他参数。但是我通过分析汇编发现,如果方法的接收者在方法中并没有真正用到的话,那么在调用方法时,会直接跳过接收者,直接传递其他参数。比如下面的例子,汇编代码中可能就会忽略demo指针的传递:

1
2
3
4
5
6
type demo struct {
a int
}
func (d *demo) Func(a, b, c, d, e, f, g, h, i, j, k, l int) (int, int, int, int, int, int, int, int, int, int, int) {
return a, b, c, d, e, f, g, h, i, j, k // 这里没有用到demo指针
}