指针传递一定比值传递效率高?

129 阅读5分钟

golang中,无论函数还是方法都少不了参数的传递,传递的方式无非两种 值传递和指针传递。另外不止参数,函数或方法的返回值也允许值传递或者指针传递,以及变量内容的赋值、修改都少不了要进行值传递还是指针传递的选择。

值传递与指针传递的区别

按照朴素的理解,指针就是一个32位或者64位的数据,存储的是实际指向的对象或者数据的地址。而值存储的就确确实实是对象或者数据,所以一般需要分配较大的内存空间。 28588B40-C7EC-48A5-833D-F11C3CCE0497.png 结合图示不难看出,指针传递要比值传递少分配很大的内存空间,并且它不需要把参数的整个内存数据块复制,仅仅复制参数的地址就可以,很自然的可以得出结论指针传递一定比值传递效率高,那事实情况下一定是这样吗?

数据的存储区域

栈空间(Stack)

主要用于存储函数调用过程中的局部变量、函数参数以及返回地址等信息,每个线程都有自己独立的栈空间, 这部分内存由编译器进行管理,编译时确定分配内存的大小。

栈空间优势是自动管理、快速分配和回收、有限大小和临时生命周期等。

堆空间(Heap)

堆空间用于存储动态分配的内存,无限大小,可以动态进行分配和调整,尤其编译期无法知道分配多少大小的变量,在运行时会被分配到堆上,堆空间通常用于存储程序运行期间动态创建的对象,这些对象的生命周期可能超过函数调用的上下文。Go 中,堆空间的内存回收由垃圾回收器(GC)自动处理。垃圾回收器会在适当的时机自动回收不再使用的堆上的内存。然而,垃圾回收可能会导致程序性能下降,因为它需要在运行时检查和回收不再使用的内存。

备注:栈是线程级别的,堆是进程级别的。

内存逃逸

请看下面的示例,Draw函数返回值,也就是通过值传递返回给调用者。

func Draw(x, y float64) Point {
  p := Point{x, y}
  return p
}

如果采用这种值传递的方式,首先p会在栈上申请内存,并复制p的数据分别到return_r0,return_r0同样在栈上返回,然后p会被回收。

如果把上面的例子改成指针传递

func Draw(x, y float64) *Point {
  	p := Point{x, y}
	return &p
}

改成指针传递之后,情况就变得复杂,跟原来不一样了,首先执行Draw函数内部的p必须分配在堆上,因为按照指针传递的方式会执行return_r0 := &p,即return_r0会指向p的内存空间,如果p在栈上的话,函数返回后p出栈,内存销毁,那return_r0的指向就会有问题,所以为了解决这种问题,golang引入了内存逃逸分析来决定变量应该分配在堆上还是栈上。 7B24D1D4-A6B6-420F-89F7-74C02FEDCE62.png

可能会产生内存逃逸的场景:

  • 局部变量赋值给全局变量指针
  • 调用反射(未知类型)(fmt案例的第一个问题)。
  • 已经逃逸的变量引用了其它指针,那么被引用的指针一定发生逃逸
  • 指针类型slice、map、chan中存储的变量,例如
// p1、p2不会内存逃逸
func BuildLine() {
	line := make([]Point, 2, 2)
	var p1 Point = Point{100, 200}
	var p2 Point = Point{100, 208}
	line = append(line, p1, p2)
	// 绘制line
}
// p1、p2发生内存逃逸
func BuildLine() {
	line := make([]*Point, 0, 2)
	var p1 Point = Point{100, 200}
	var p2 Point = Point{100, 208}
	line = append(line, &p1, &p2)
	// 绘制line
}

大致的意思表明一个原则:只要局部变量不能证明在函数结束后不能被引用,那么就分配到堆上。换句话说,如果局部变量被其他函数所捕获,那么就被分配到堆上。

由上可知,根据栈空间的特性可以知道,函数栈帧的大小是有限的且在编译时就已经确定,如果在编译时无法确定变量大小或者变量过大,在 runtime 运行时分配到堆上。

分析工具

// go build -gcflags '-m -l' xxxx.go 
// go tool compile -S xxxx.go

内存逃逸的影响

从上面的分析可以知道,发生内存逃逸时,对象一般会在堆空间上分配。如果是高并发大吞吐场景下堆上分配内存会产生比较大的影响。

  1. 性能影响:在堆上分配内存通常比在栈上分配内存要慢,因为它涉及到更复杂的内存管理。因此,过多的内存逃逸可能会导致程序性能下降。
  2. 垃圾回收影响:在堆上分配的内存需要由垃圾回收器(GC)进行回收,而栈上的内存在函数返回时会被自动回收。因此,过多的内存逃逸可能会增加垃圾回收的负担,从而影响程序性能。
  3. 并发冲突影响:在高并发中,过多的内存逃逸可能会导致数据竞争和同步问题。例如,如果一个 goroutine 持有一个在堆上分配的对象的引用,而另一个 goroutine 试图修改这个对象,就可能发生数据竞争。
  4. 内存使用影响:过多的内存逃逸可能会导致程序使用更多的内存,因为堆上的内存不像栈上的内存那样能被及时回收。堆上的内存回收通常在一定策略下会慢一些。

CA81E9B5-0E3F-450C-9225-2C1157BA94C3.png

结论

回到题目,过多的指针传递其实可能并不一定能提升效率,有时候反而会影响程序性能,尤其在大量小对象场景下。