函数传递指针真的比传值效率高吗

2,027 阅读4分钟

一个函数可以接收值类型的变量也可以接收指针类型的变量,有人说,指针类型的更好,这样就不用多余的值拷贝了,不管这个指针怎么传,我们始终用的都是指针指向那块内存。然而真的是这样的吗?
我们知道内存的分配可以在堆上也可以在栈上,当然内存在栈上分配更快,并且栈上的内存不需要GC,入栈出栈直接回收。go在编译期间会对变量进行分析,到底一个变量分配在栈上更好还是堆上更好。
我们可以通过 go run -gcflags "-m -l" xx.go 来分析go是如何分配内存的

case 1

image.png image.png getUser函数内部先初始化一个user变量,然后返回user的指针,虽然user是在函数内存创建的,但是返回的是指针,那么go认为这个user可以会被外部使用,所以一开始在创建user的时候就不在栈上申请内存了,直接在堆上申请。

case 2

image.png

image.png makesomething内部先初始化一个1000长度的slice,并没有发生逃逸

image.png

image.png makesomething内部先初始化一个10000长度的slice,发生逃逸

栈的空间有限,go在编译期间会判断如果变量的初始化分配内存较大时,会直接在堆上分配。

case 3

image.png

image.png 创建一个类型是指针类型的slice,然后对其中的成员赋值,我们发现b逃逸了,a没有逃逸。在一个切片上存储指针或者带有指针的值,会发生逃逸。当然此时如果我们在结尾return a,那么a也会发生逃逸。

case 4

image.png

image.png 创建一个类型是指针类型的chan,然后把b的地址推进去,b发生逃逸

case 5

image.png

image.png 创建一个value是指针类型的map,然后把b的地址设置成a[0]的value,b发生逃逸

case 6

image.png

image.png d是实现了Animal interface的dog,在调用d.Speak的时候,dog发生了逃逸,在interface类型上调用方法,只有在真正执行的时候才会知道,所以选择逃逸。

case 7

carbon (11).png

image.png 虽然make int的slice的设置的容量是1,但是因为用变量t代替了1,这样编译器就选择逃逸。

case 8

carbon-2.png

image.png 先初始化两个指针类型的变量a,c,然后for循环给a赋值&b,b逃逸,闭包给c赋值&f,f逃逸。

case 9

carbon-4.png image.png test 返回一个func,func f内部只是重新赋值a,b,也会发生逃逸

case 10

carbon-5.png

carbon-6.png 这些print函数都会逃逸,它们底层走的Fprintln,因为本身接收的类型是interface,而p返回的是指针,那么p肯定会逃逸,通过p.doPrintln,a又赋值给了p的某个成员,所以a逃逸。

总结:

  1. 函数返回变量的指针时,这个变量会逃逸
  2. 当觉得栈上的空间不够时,会分配在堆上
  3. 在切片上存储指针或带指针的值的时候,对应的变量会逃逸
  4. chan里面的元素是指针的时候,也会发生逃逸
  5. map的value是指针的时候,也会发生逃逸
  6. 在interface类型上调用方法,也会发生逃逸
  7. 当给一个slice分配一个动态的空间容量时,也会发生逃逸
  8. 函数或闭包外声明指针,在函数或闭包内分配,也会发生逃逸
  9. 函数外初始化变量,函数内使用变量,然后返回函数,也会发生逃逸
  10. 被已经逃逸的指针引用的指针,也会发生逃逸
  11. 逃逸分析在编译阶段完成的
  12. 逃逸分析的好处可以判断变量放在堆上还是栈上,分配在栈上的变量对GC友好

让我们来看看一开始的问题:函数传递指针真的比传值效率高吗? carbon (12).png

image.png

  1. t0函数初始化一个num
  2. t0把num的地址传给t1
  3. t1初始化一个user的指针
  4. t1内u的成员num指向了t0的num地址
  5. 返回指针类型的user 因为t1返回user指针,user逃逸没问题,user的num指向了t0num的地址,导致了t0的num发生了逃逸,原本在栈区好好的t0的num,跑到了堆区,gc又忙了。所以函数传递指针真的不一定比传值效率高。