Golang的变量内存分配和逃逸分析

  • Post author:
  • Post category:golang


  • 内存分配方式
  • 逃逸分析
  • 总结



一、内存分配方式

一个执行的程序在内存中的分配总共有五部分:

1、text:存储程序的二进制指令和一些静态内容

2、data:存储已经初始化的全局变量,静态分配

3、bss:存储未被初始化的全局变量,静态分配

4、stack:栈,主要用来函数调用时存储局部变量。内存由系统管理,通过压栈的方式自动分配和通过出栈的方式自动释放。

5、heap:堆,用于动态分配内存,由垃圾回收器管理。


Golang的分配方式主要是栈分配和堆分配。


栈分配廉价,堆分配昂贵。

  • 栈:可以只通过压栈和出栈两个CPU指令来实现栈空间的分配和释放,属于静态分配内存。
  • 堆:使用堆进行动态分配后,在释放堆空间时,需要使用垃圾回收器来扫描寻找堆空间中不再被使用的对象。

使用栈分配:函数内部的局部变量未被外部引用,即变量在编译期间便可确定作用域。

只能使用堆分配:(1)函数内部的局部变量被外部引用。(2)当变量需要申请的内存过大而超过栈的存储能力时。



二、逃逸分析

通过命令

go build -gcflags ‘-m’ ./main.go

,可以多传几个

-m

来查看更详细的内容,

-l

可以禁止内联优化。

指针逃逸:一个对象的指针被多个线程或方法引用。

逃逸分析:Golang编译器分析指针对象动态范围的方法。逃逸分析决定了变量的内存分配在栈或是堆。

编译器通过在编译时进行垃圾回收的工作来选择某个变量的内存分配方式。编译器通过追踪变量在代码块中的作用域,变量携带的校验数据可以验证其生命周期在运行时是否完全可知,如果是,便可以在栈上分配内存。否则称该变量逃逸,只能通过堆来分配内存。



引起变量逃逸到堆上的典型情况:

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

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

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

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



(1)指针的情况:

指针指向的数据都是在堆上分配的。

传统上,认为值拷贝比指针的代价高。但在诸多实际情况中,值拷贝要比使用指针廉价得多。

1、编译器会在解除指针时做检查。目的是在指针是 nil 的情况下直接 panic() 以避免内存泄露。这就必须在运行时执行更多的代码。如果数据是按值传递的,那就不需要做这些了,它不可能是 nil

2、指针通常有糟糕的局部引用。一个函数内部的所有值都会在栈空间上分配。局部引用是编写高效代码的重要环节。它会使得变量数据在 CPU Cache(cpu 的一级二级缓存) 中的热度更高,进而减少指令预取时 Cache 不命中的的几率。

3、在 Cache 层拷贝一堆对象,可粗略地认为和拷贝一个指针效率是一样的。CPU 在各 Cache 层和主内存中以固定大小的 cache 进行内存移动。x86 机器上是 64 字节。而且,Go 使用了Duff’s device 技术来使得常规内存操作变得更高效。

垃圾回收器回收一个变量,要检查变量类型里是否有指针,如果有,要检查指针所指向的内存是否可被回收,进而才能决定这个变量能否被回收。如此递归下去。

如果被回收的变量里面没有指针, 就不需要进去递归扫描了,直接回收掉就行。



(2)传递slice

切片是造成低效内存分配行为的狂热区域。除非切片的大小在编译时就能知道,否则切片背后的数组(map也一样)会在堆上分配。(string是只读的[]byte)



(3)Interface类型

在 Interface 类型上调用方法要比直接在 Struct 上调用方法效率低。在 interface 类型上调用方法是动态调度的。



(4)uintptr、unsafe.Pointer

Uintptr产生的引用和unsafe.Pointer引用的数据,编译器不认为会出现逃逸。很高效,但是不安全。开发者应该慎用。



三、总结

1、不要过早优化,用数据来驱动我们的优化工作。

2、栈空间分配是廉价的,堆空间分配是昂贵的。

3、了解逃逸机制可以让我们写出更高效的代码。

4、指针的使用会导致栈分配更不可行。

5、找到在低效代码块中提供分配控制的 api。(没有被封装的最底层的方法)

6、在调用频繁的地方慎用 interface。

参考文章:

高性能 Go 服务的内存优化(译)

本文有关逃逸分析摘录了参考文章的诸多内容以作归纳和总结。参考文章中有详细的示例代码可作参考。



版权声明:本文为studyhard232原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。