Go语言变量逃逸分析

在讨论变量生命周期之前,先来了解下计算机组成里两个非常重要的概念:堆和栈。变量的生命周期我们将在下一节《变量生命周期》中为大家讲解。

什么是栈

栈(Stack)是一种拥有特殊规则的线性表数据结构

1) 概念

栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,如下图所示。


图:栈的操作及扩展

往栈中放入元素的过程叫做入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。

从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的元素数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除了栈顶部的成员)进行任何查看和修改操作。

栈的原理类似于将书籍一本一本地堆起来。书按顺序一本一本从顶部放入,要取书时只能从顶部一本一本取出。

2) 变量和栈有什么关系

栈可用于内存分配,栈的分配和回收速度非常快。下面的代码展示了栈在内存分配上的作用:
func calc(a, b int) int {
    var c int
    c = a * b

    var x int
    x = c * 10

    return 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语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。

1) 逃逸分析

通过下面的代码来展现Go语言如何使用命令行来分析变量逃逸,代码如下:
package main

import "fmt"

// 本函数测试入口参数和返回值情况
func dummy(b int) int {

    // 声明一个变量c并赋值
    var c int
    c = b

    return 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 行告知“代码的第 29 行的变量 a 逃逸到堆”。
  • 第 3 行告知“dummy(0) 调用逃逸到堆”。由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在 main() 函数中继续存在。
  • 第 4 行,这句提示是默认的,可以忽略。

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

2) 取地址发生逃逸

下面的例子使用结构体做数据,来了解结构体在堆上的分配情况,代码如下:
package main

import "fmt"

// 声明空结构体测试结构体逃逸情况
type Data struct {
}

func dummy() *Data {
    // 实例化c为Data类型
    var c Data

    //返回函数局部变量地址
    return &c
}

func main() {
    fmt.Println(dummy())
}
代码说明如下:
  • 第 6 行,声明一个空的结构体做结构体逃逸分析。
  • 第 9 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
  • 第 11 行,将变量 c 声明为 Data 类型,此时 c 的结构体为值类型。
  • 第 14 行,取函数局部变量 c 的地址并返回。
  • 第 18 行,打印 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() 函数的返回值将是一个不可预知的内存地址,这种情况一般是 C/C++ 语言中容易犯错的地方,引用了一个函数局部变量的地址。

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

3) 原则

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

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

推荐文章
File类(在JSP中的使用)详解

File对象用来获取文件本身的一些信息,如文件所在的目录、文件的长度、文件读/写权限等,但File对象并不涉及对文件的读/写操作。 创建File对象的构造方法有三个: File(Stringfi

jQuery创建元素节点

在jQuery中,我们可以采用字符串的形式来创建一个元素节点,再通过append()、before()等方法把这个字符串插入到现有的元素节点中。 语法: //方式1 varstr="字符串";

Maven搭建SSM(SpringMVC+Spring+MyBatis)框架入门教程(超级详细)

MavenSpringHibernate 创建公共POM 1.SpringMVCPOMServlet 4.0.0 cn.com.mvn.pom SpringPOM 0.0.1-SNAP

20个适合前端开发/设计的 ICON图标库

在应用界面中图标的存在,会给用户一个良好第一印象,这个挺重要的。但是从零开始开发图标需要大量的时间和金钱。在你的网站上使用图标可以让你有效地与你的访问者交流:它们增加了可读性,突出了重要的内容,加强了

Java List.remove()方法:移出列表中的指定元素

Java 集合类中的List.remove()方法用于移出列表中的指定元素。返回值为被移除的元素。 语法1 remove(intindex) 参数说明: index:要先移除的元素的索引。

Java匿名类,Java匿名内部类

匿名类是指没有类名的内部类,必须在创建时使用new语句来声明类。其语法形式如下: new(){ //类的主体 }; 这种形式的new语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一

Qt QTreeWidget和QDockWidget用法完全攻略(实例分析)

QTreeWidgetQDockWidget 图1实例Samp4_8运行时界面 QTreeWidget目录树组件:QTreeWidget类是创建和管理目录树结构的类。实例使用一个QTreeWid

ai cs6是什么?

AIcs6是Adobe系统公司推出的基于矢量的图形制作软件Adobeillustrator的CS6版本。AdobeIllustratorCS6在增加大量功能和问题修复之外,最主要的是通过AdobeMe

Python基础教程(入门教程),30分钟玩转Python编程!

这是一篇针对初学者的Python基础教程,只要你认真阅读,花费30分钟即可快速了解Python。 这篇Python入门教程讲解的知识点包括:Python编程环境的搭建、Python基本操作入门、Py

Django Model三种继承模型详解

在Django中每个Model都是一个Pyhton类,前文之前提到过Model继承自django.db.models.Model。通过类之间的继承Django会对自定义的Model自动添加了两个属性分

有损压缩格式有哪些

有损压缩是利用了人类对图像或声波中的某些频率成分不敏感的特性,允许压缩过程中损失一定的信息;虽然不能完全恢复原始数据,但是所损失的部分对理解原始图像的影响缩小,却换来了大得多的压缩比。有损压缩广泛应用

函数的传参方式有哪些?

函数传参有三种传参方式:传值、传址、传引用。1、按值传递(1)形参和实参各占一个独立的存储空间。(2)形参的存储空间是函数被调用时才分配的,调用开始,系统为形参开辟一个临时的存储区,然后将各实参传递给

C语言冒泡排序算法

用冒泡排序法对任意输入的10个数按照从小到大的顺序进行排序。 实现过程: (1)通过两个for循环实现冒泡排序的全过程,外层for循环决定冒泡排序的趟数,内层for循环决定每趟所进行两两比较的次数。

JSP  ……标签体 标签属性 属性 类型 描述 引用EL items String 被循环遍历的对象,多用于数组与集合类 可以 delims String 字

TensorFlow分布式在谷歌云平台运行详解

在这个案例中将学习如何在谷歌云平台上使用GoogleTensorFlow,回顾的例子是经典的MNIST。 在https://cloud.google.com/中查看GCP的工作方式将是一件好事。请注

Java Eclipse下载安装教程

学习Java语言程序设计必须选择一个功能强大、使用简单,能够辅助程序设计的IDE。 Eclipse是目前最流行的Java语言开发工具,它强大的代码辅助功能,可以帮助开发人员自动完成语法修正、补全文字

Java截取(提取)子字符串(substring())

在String中提供了两个截取字符串的方法,一个是从指定位置截取到字符串结尾,另一个是截取指定范围的内容。下面对这两种方法分别进行介绍。 1.substring(intbeginIndex)形式 此

Autotune是一种什么软件?

Autotune是一种音高修正软件,可以让用户在不改变原始演奏音质的同时让用户对音频的音高部分进行改变和修正,而且修正后的效果会变的非常不错,让音频几乎不会出现任何的失真,很难让人听出这是一个被处理过

Android Socket编程(附带实例)

关于Socket的具体介绍和Java网络编程的介绍这一节就不过多讲解了,如果大家还不是很了解,可参考《Socket教程》和《Java网络编程教程》。 下面我们编写的小程序主要使用java.net包下

Linux LV逻辑卷(创建、查看、调整大小及删除)详解

本节讲解如何将卷组划分为逻辑卷,我们可以把逻辑卷想象成分区,那么这个逻辑卷当然也需要被格式化和挂载。另外,逻辑卷也是可以动态调整大小的,而且数据不会丟失,也不用卸载逻辑卷。 建立逻辑卷 我们现在已经