Go语言|「GCTT 出品」Go 语言机制之逃逸分析


Go语言|「GCTT 出品」Go 语言机制之逃逸分析文章插图
前序(Prelude)本系列文章总共四篇 , 主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则 , 包括指针、栈、堆、逃逸分析和值/指针传递 。 这是第二篇 , 主要介绍堆和逃逸分析 。
以下是本系列文章的索引:

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