Go 语言机制之逃逸分析


Go 语言机制之逃逸分析

文章插图
 
前序(Prelude)本系列文章总共四篇,主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则,包括指针、栈、堆、逃逸分析和值/指针传递 。这是第二篇,主要介绍堆和逃逸分析 。
以下是本系列文章的索引:
  1. 「GCTT 出品」Go 语言机制之栈和指针
  2. Go 语言机制之逃逸分析
  3. Go 语言机制之内存剖析
  4. Go 语言机制之数据和语法的设计哲学
介绍(Introduction)在四部分系列的第一部分,我用一个将值共享给 goroutine 栈的例子介绍了指针结构的基础 。而我没有说的是值存在栈之上的情况 。为了理解这个,你需要学习值存储的另外一个位置:堆 。有这个基础,就可以开始学习逃逸分析 。
逃逸分析是编译器用来决定你的程序中值的位置的过程 。特别地,编译器执行静态代码分析,以确定一个构造体的实例化值是否会逃逸到堆 。在 Go 语言中,你没有可用的关键字或者函数,能够直接让编译器做这个决定 。只能够通过你写代码的方式来作出这个决定 。
堆(Heaps)
堆是内存的第二区域,除了栈之外,用来存储值的地方 。堆无法像栈一样能自清理,所以使用这部分内存会造成很大的开销(相比于使用栈) 。重要的是,开销跟 GC(垃圾收集),即被牵扯进来保证这部分区域干净的程序,有很大的关系 。当垃圾收集程序运行时,它会占用你的可用 CPU 容量的 25% 。更有甚者,它会造成微秒级的 “stop the world” 的延时 。拥有 GC 的好处是你可以不再关注堆内存的管理,这部分很复杂,是历史上容易出错的地方 。
在 Go 中,会将一部分值分配到堆上 。这些分配给 GC 带来了压力,因为堆上没有被指针索引的值都需要被删除 。越多需要被检查和删除的值,会给每次运行 GC 时带来越多的工作 。所以,分配算法不断地工作,以平衡堆的大小和它运行的速度 。
共享栈(Sharing Stacks)
在 Go 语言中,不允许 goroutine 中的指针指向另外一个 goroutine 的栈 。这是因为当栈增长或者收缩时,goroutine 中的栈内存会被一块新的内存替换 。如果运行时需要追踪指针指向其他的 goroutine 的栈,就会造成非常多需要管理的内存,以至于更新指向那些栈的指针将使 “stop the world” 问题更严重 。
这里有一个栈被替换好几次的例子 。看输出的第 2 和第 6 行 。你会看到 main 函数中的栈的字符串地址值改变了两次 。https://play.golang.org/p/pxn5u4EBSI
逃逸机制(Escape Mechanics)
任何时候,一个值被分享到函数栈帧范围之外,它都会在堆上被重新分配 。这是逃逸分析算法发现这些情况和管控这一层的工作 。(内存的)完整性在于确保对任何值的访问始终是准确、一致和高效的 。
通过查看这个语言机制了解逃逸分析 。https://play.golang.org/p/Y_VZxYteKO
清单 1
Go 语言机制之逃逸分析

文章插图
 
我使用 go:noinline 指令,阻止在 main 函数中,编译器使用内联代码替代函数调用 。内联(优化)会使函数调用消失,并使例子复杂化 。我将在下一篇博文介绍内联造成的副作用 。
在表 1 中,你可以看到创建 user 值,并返回给调用者的两个不同的函数 。在函数版本 1 中,返回值 。
清单 2
Go 语言机制之逃逸分析

文章插图
 
我说这个函数返回的是值是因为这个被函数创建的 user 值被拷贝并传递到调用栈上 。这意味着调用函数接收到的是这个值的拷贝 。
你可以看下第 17 行到 20 行 user 值被构造的过程 。然后在第 23 行,user 值的副本被传递到调用栈并返回给调用者 。函数返回后,栈看起来如下所示 。
图 1
Go 语言机制之逃逸分析

文章插图
 
你可以看到图 1 中,当调用完 createUserV1 ,一个 user 值同时存在(两个函数的)栈帧中 。在函数版本 2 中,返回指针 。
清单 3
Go 语言机制之逃逸分析

文章插图
 
我说这个函数返回的是指针是因为这个被函数创建的 user 值通过调用栈被共享了 。这意味着调用函数接收到一个值的地址拷贝 。
你可以看到在第 28 行到 31 行使用相同的字段值来构造 user 值,但在第 34 行返回时却是不同的 。不是将 user 值的副本传递到调用栈,而是将 user 值的地址传递到调用栈 。基于此,你也许会认为栈在调用之后是这个样子 。


推荐阅读