golang 内存逃逸和uintptr,unsafe.Pointer

上半部分参考 https://www.jianshu.com/p/63404461e520

golang 在栈或者堆中分配内存,更倾向在栈中分配因为代价低,

 

内存逃逸上指,编译器编译时检查变量,发现整个生命周期是否在运行时完全可知。

如果可知,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。

可以使用命令

go build -gcflags=-m

  进行逃逸分析

 

通常栈逃逸到堆的情况

  • 发送指针或带有指针的值到 channel 中。在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。

  • 在一个切片上存储指针或带指针的值。一个典型的例子就是 []*string。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。

  • slice 的背后数组被重新分配了,因为 append 时可能会超出其容量(cap)。slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。

  • 在 interface 类型上调用方法。在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r, 调用 r.Read(b) 会使得 r 的值和切片 b 的背后存储都逃逸掉,所以会在堆上分配。

也可以主动避免内存逃逸,使用 noescape()函数,函数源码如下
 func noescape(p unsafe.Pointer) unsafe.Pointer {
     x := uintptr(p)
     return unsafe.Pointer(x ^ 0)
}

  

  • noescape() 函数的作用是遮蔽输入和输出的依赖关系。使编译器不认为 p 会通过 x 逃逸, 因为 uintptr() 产生的引用是编译器无法理解的。

  • 内置的 uintptr 类型是一个真正的指针类型,但是在编译器层面,它只是一个存储一个 指针地址 的 int 类型。代码的最后一行返回 unsafe.Pointer 也是一个 int

  • noescape() 在 runtime 包中使用 unsafe.Pointer 的地方被大量使用。如果作者清楚被 unsafe.Pointer 引用的数据肯定不会被逃逸,但编译器却不知道的情况下,这是很有用的。

 

以上原文都有,然后主要说说 uintptr 和 unsafe.Pointer

  • unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
  • 而uintptr是用于指针运算的,GC 不把 uintptr 当指针,它只是一个存储一个 指针地址 的 int 类型,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
  • unsafe.Pointer 可以和 普通指针 进行相互转换;
  • unsafe.Pointer 可以和 uintptr 进行相互转换。

可以使用此指针方式给结构体赋值

type Test struct {
	a int
	b int
}

func main()  {

        var test *Test = new(Test)
	fmt.Println(test)

	a := unsafe.Pointer(uintptr(unsafe.Pointer(test))+unsafe.Offsetof(test.a))
	*((*int)(a)) = 21
	fmt.Println(test)


}    

  运行结果

&{0 0}
&{21 0}

 uintptr(unsafe.Pointer(test)) 获取了 test 的指针起始值,然后offsetof获取偏移量,a为结构体内a的指针,是一个通用指针

转换为int指针,再使用* 解引用,赋值

 

posted @ 2020-09-17 14:52  海拉尔  阅读(527)  评论(0编辑  收藏  举报