变量和栈有什么关系

栈可用于内存分配,栈的分配和回收速度非常快。下面代码展示栈在内存分配上的作用,代码如下:

func calc(a, b int) int {var c intc = a * bvar x intx = c * 10return x
}

代码说明如下:
第 1 行,传入 a、b 两个整型参数。
第 2 行,声明 c 整型变量,运行时,c 会分配一段内存用以存储 c 的数值。
第 3 行,将 a 和 b 相乘后赋予 c。
第 5 行,声明 x 整型变量,x 也会被分配一段内存。
第 6 行,让 c 乘以 10 后存储到 x 变量中。
第 8 行,返回 x 的值。

上面的代码在没有任何优化情况下,会进行 c 和 x 变量的分配过程。Go 语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。

变量逃逸(Escape Analysis)——自动决定变量分配方式,提高运行效率

堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈;全局变量、结构体成员使用堆分配等。程序员不得不花费很多年的时间在不同的项目中学习、记忆这些概念并加以实践和使用。

Go 语言将这个过程整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配,即使程序员使用 Go 语言完成了整个工程后也不会感受到这个过程。

  1. 逃逸分析
    使用下面的代码来展现 Go 语言如何通过命令行分析变量逃逸,代码如下:
package mainimport "fmt"// 本函数测试入口参数和返回值情况
func dummy(b int) int {// 声明一个c赋值进入参数并返回var c intc = breturn c
}// 空函数, 什么也不做
func void() {}func main() {// 声明a变量并打印var a int// 调用void()函数void()// 打印a变量的值和dummy()函数返回fmt.Println(a, dummy(0))
}

代码说明如下:

  • 第 6 行,dummy() 函数拥有一个参数,返回一个整型值,测试函数参数和返回值分析情况。
  • 第 9 行,声明 c 变量,这里演示函数临时变量通过函数返回值返回后的情况。
  • 第 16 行,这是一个空函数,测试没有任何参数函数的分析情况。
  • 第 23 行,在 main() 中声明 a 变量,测试 main() 中变量的分析情况。
  • 第 26 行,调用 void() 函数,没有返回值,测试 void() 调用后的分析情况。
  • 第 29 行,打印 a 和 dummy(0) 的返回值,测试函数返回值没有变量接收时的分析情况。

接着使用如下命令行运行上面的代码:

$ go run -gcflags "-m -l" main.go

使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化。

运行结果如下:

\# command-line-arguments
./main.go:29:13: a escapes to heap
./main.go:29:22: dummy(0) escapes to heap
./main.go:29:13: main ... argument does not escape
0 0

程序运行结果分析如下:

  • 输出第 2 行告知“main 的第 29 行的变量 a 逃逸到堆”。
  • 第 3 行告知“dummy(0)调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在其声明后继续在 main() 函数中存在。
  • 第 4 行,这句提示是默认的,可以忽略。

上面例子中变量 c 是整型,其值通过 dummy() 的返回值“逃出”了 dummy() 函数。c 变量值被复制并作为 dummy() 函数返回值返回,即使 c 变量在 dummy() 函数中分配的内存被释放,也不会影响 main() 中使用 dummy() 返回的值。c 变量使用栈分配不会影响结果。

  1. 取地址发生逃逸
    下面的例子使用结构体做数据,了解在堆上分配的情况,代码如下:
package mainimport "fmt"// 声明空结构体测试结构体逃逸情况
type Data struct {
}func dummy() *Data {// 实例化c为Data类型var c Data//返回函数局部变量地址return &c
}func main() {fmt.Println(dummy())
}

代码说明如下:
第 6 行,声明一个空的结构体做结构体逃逸分析。
第 9 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
第 12 行,将 c 变量声明为 Data 类型,此时 c 的结构体为值类型。
第 15 行,取函数局部变量 c 的地址并返回。Go 语言的特性允许这样做。
第 20 行,打印 dummy() 函数的返回值。

执行逃逸分析:

$ go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:15:9: &c escapes to heap
./main.go:12:6: moved to heap: c
./main.go:20:19: dummy() escapes to heap
./main.go:20:13: main ... argument does not escape
&{}

注意第 4 行出现了新的提示:将 c 移到堆中。这句话表示,Go 编译器已经确认如果将 c 变量分配在栈上是无法保证程序最终结果的。如果坚持这样做,dummy() 的返回值将是 Data 结构的一个不可预知的内存地址。这种情况一般是 C/C++ 语言中容易犯错的地方:引用了一个函数局部变量的地址。

Go 语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存。

  1. 原则
    在使用 Go 语言进行编程时,Go 语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆上的问题。编译器会自动帮助开发者完成这个纠结的选择。但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于 Go 语言,在 Java 等语言的编译器优化上也使用了类似的技术。

编译器觉得变量应该分配在堆和栈上的原则是:
变量是否被取地址。
变量是否发生逃逸

生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

例如,下面摘录的部分代码片段:

for t := 0.0; t < cycles*2*math.Pi; t += res {x := math.Sin(t)y := math.Sin(t*freq + phase)img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),blackIndex)
}

提示:函数的有右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样:

for t := 0.0; t < cycles*2*math.Pi; t += res {x := math.Sin(t)y := math.Sin(t*freq + phase)img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性)               // 小括弧另起一行缩进,和大括弧的风格保存一致
}

在每次循环的开始会创建临时变量 t,然后在每次循环迭代中创建临时变量 x 和 y。

那么 Go语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用 var 还是 new 声明变量的方式决定的。

var global *int
func f() {var x intx = 1global = &x
}
func g() {y := new(int)*y = 1
}

f 函数里的 x 变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的;用 Go语言的术语说,这个 x 局部变量从函数 f 中逃逸了。

相反,当 g 函数返回时,变量 *y 将是不可达的,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间(译注:也可以选择在堆上分配,然后由 Go语言的 GC 回收这个变量的内存空间),虽然这里用的是 new 方式。

其实在任何时候,并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

golang逃逸分析相关推荐

  1. 记录一次Golang逃逸分析

    今天偶然看到Golang关于内存的文章,其中涉及了一点逃逸分析,由于去年之前都是专研C++,Golang也是去年11月才开始学习的,学完就马上进入项目了,没有深究底层,准备这段时间边改论文边开始仔细学 ...

  2. golang int 转string_Golang的逃逸分析

    逃逸分析 逃逸分析(Escape Analysis)指的是将变量的内存分配在合适的地方(堆或者栈). 在函数中申请内存有2种情况: - 如果内存分配在栈(stack)上,当函数退出的时候,这部分内存会 ...

  3. GoLang的逃逸分析

    GoLang的垃圾回收机制可以进行自动内存管理让我们的代码更简洁,同时发生内存泄漏的可能性更小.然而,GC会定期停止并收集未使用的对象,因此还是会增加程序的开销.Go的编译器十分聪明,比如决定变量需要 ...

  4. Golang 内存分配与逃逸分析

    参考:灵魂拷问:Go 语言这个变量到底分配到哪里了? 来源于 公众号: 脑子进煎鱼了 ,作者陈煎鱼. 我们在写代码的时候,有时候会想这个变量到底分配到哪里了?这时候可能会有人说,在栈上,在堆上.信我准 ...

  5. 通过实例理解 Go 逃逸分析

    本文转载自白明老师,这是中文社区里面最好.最全面的一篇关于逃逸分析的文章,写得非常好.既有理论.又有实践,引经据典,精彩至及. 翻看了一下自己的Go文章归档[1],发现自己从未专门写过有关Go逃逸分析 ...

  6. Go内存管理之代码的逃逸分析

    基本上,每种编程语言都有其自己的内存模型.每个变量,常量都存储在内存的某个物理位置上,这些存储位置通过内存指针访问. 至于变量,就是程序里赋予内存存储位置的名称.程序可以根据需要进行操作,并且可以将新 ...

  7. 通过实例理解Go逃逸分析

    翻看了一下自己的Go文章归档[1],发现自己从未专门写过有关Go逃逸分析(escape analysis)的文章.关于Go变量的逃逸分析,大多数Gopher其实并不用关心,甚至可以无视.但是如果你将G ...

  8. JVM---堆(逃逸分析与代码优化)

    堆-逃逸分析 堆是分配对象的唯一选择么? 在<深入理解Java虚拟机>中关于Java堆内存有这样一段描述: 随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配.标量替换优化技术将会导致 ...

  9. 如果面试官问你 JVM,额外回答逃逸分析技术会让你加分!

    我在面试别人的过程中,JVM 内存模型我几乎必问,虽然有人说问这些就是面试造航母,工作拧螺丝.如果你想当一名 CRUD 码农,你可以选择不用了解这些. 在 JVM 内存模型的问答中,有些人能说出对象是 ...

最新文章

  1. win10用什么软件测试硬件,Win10系统下硬件设备检测工具的使用方法
  2. ATSS : 目标检测的自适应正负anchor选择,很扎实的trick | CVPR 2020
  3. 5月15日直播预告:英飞凌AURIX™培训—图像处理、实车演示等热点问题
  4. Flutter开发Flutter与原生OC、Java的交互通信-2(48)
  5. 安卓期末作品小项目_北京部编版八年级上册语文期末试卷
  6. Mysql的IP转换
  7. 基于python3的Opencv(一)-打开摄像头显示图像
  8. linux命令无视错误,llinux 的一些命令和错误
  9. 线性运算和非线性运算
  10. 树莓派超声波车牌识别系统
  11. 一个图文混排问题的解决过程
  12. Yale CAS + .net Client 实现 SSO(2)
  13. adb命令——简单常用命令介绍:截图——adb shell screencap -p /sdcard/123.png...
  14. 数字信号处理前瞻(note1):奈奎斯特与折叠频率
  15. 通过端口查看进程和通过进程查看端口
  16. java ini_Java读取ini文件 [org.dtools.javaini] | 学步园
  17. csm和uefi_[整理]BIOS设置UEFI和安全引导
  18. 计算机断电重启后蓝屏,电脑断电后重启屏幕出现蓝屏代码0x000000f4解决方法
  19. conda create出现连接问题_处理conda安装工具的动态库缺失问题
  20. q群机器人php,QQ机器人接口(加群可见)

热门文章

  1. 我的 Serverless 实战 — 云函数与触发器的创建与使用 ( 开通腾讯云 “ 云开发 “ 服务 | 创建云函数 | 创建触发器 | 测试触发器 )
  2. 【错误记录】p7zip 交叉编译 Android 版本 NDK 报错 ( Application.mk | APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 )
  3. 【Flutter】Image 组件 ( 配置本地 gif 图片资源 | 本地资源加载 placeholder )
  4. 【计算理论】计算理论总结 ( 非确定性有限自动机 NFA 转为确定性有限自动机 DFA | 示例 ) ★★
  5. 【组合数学】集合的排列组合问题示例 ( 排列 | 组合 | 圆排列 | 二项式定理 )
  6. Spring @CrossOrigin 通配符 解决跨域问题
  7. ASP.NET Core WebAPI中的分析工具MiniProfiler
  8. 彻底删除SharePoint 2010 Content Database
  9. Chrome 的又一个bug?
  10. 今天才发现ff不支持navigate。