0. 前言

小白学标准库之 reflect 篇中介绍了反射的三大法则以及变量的逃逸分析。对于逃逸分析的介绍不多,大部分都是引自 Go 逃逸分析。不过后来看反射源码的过程中发现有一种情况 Go 逃逸分析 没讲透。且当时没从底层汇编的角度去看,导致有种似懂非懂的感觉。这里就变量逃逸内容进行介绍。

1. 逃逸分析案例

这里的案例不同于 Go 逃逸分析,当然所属情况是其中概括的几种类型。

1.1 全局变量和局部变量

示例代码:

var a int

func main() {
x := 10
a = x
println(a, x)
}

代码非常简单,定义全局变量 a 和局部变量 x,然后调用 println 打印 a 和 x。

使用 go tool compile 查看编译情况,注意变量逃逸在编译阶段,而不是运行时确定的。所以这里用 go tool compile 是能确定变量逃逸情况的:

// -m 查看变量逃逸情况
$ go tool compile -m escape.go
escape.go:5:6: can inline main
escape.go:39:6: can inline escapes
escape.go:39:14: leaking param: x // 使用 -l 关闭函数内联
$ go tool compile -m -l escape.go
escape.go:39:14: leaking param: x

打印 leaking param: x 表明 x 代码中并未对 x 做任何引用操作,x 是一个泄露参数。不过,对于变量逃逸分析不影响,从结果来看,全局变量和局部变量都是在栈上分配的。

进一步思考,为什么全局变量会在栈上分配呢?

因为对全局变量赋值是传值的,传值就意味着这个值不是原有值,是值的拷贝。所以原有值不需要逃逸到堆上,只需要在栈上做变量拷贝就行。

改写示例代码如下:

var a *int

func main() {
x := 10
a = &x
println(a, x)
}

将全局变量改为全局指针类型变量,指针指向局部变量 x。查看变量逃逸情况:

$ go tool compile -l -m escape.go
escape.go:7:2: moved to heap: x

可以看到,变量 x 被 moved 到堆中。不难理解,全局变量指向 x,如果 x 不移到堆中,当 x 释放时,其它函数通过全局变量 a 找不到 x 了。事实上这是 c/c++ 语言会出现的情况。

通过汇编代码也能验证这点:

$ go tool compile -N -S -l escape.go
...
CALL runtime.newobject(SB)

继续改写上述代码:

func main() {
x := 10
a := &x
println(a, x)
}

这里用一个局部指针类型变量 a 指向 x,查看变量分配情况:

$ go tool compile -l -m escape.go

可以看到,变量 x 和 a 都是在栈上分配的。编译器检查到 a 是个指针类型变量并不会被外部作用域引用,可以将 x 放在栈上分配。

1.2 interface{} 型变量逃逸

go 接口学习笔记 中介绍了接口类型的表示。

对于 interface{} 类型的运行时表示为 runtime.eface:

type eface struct {
_type *_type
data unsafe.Pointer
}

这是空接口的运行时表示,对于编译阶段用于反射的空接口表示是 reflect.emptyInterface:

type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}

知道了 interface{} 的反射表示,我们看示例代码:

func main() {
var a int = 10
var ai interface{} = a
println(ai)
}

逃逸分析:

$ go tool compile -l -m escape.go

改写示例代码:

func main() {
var a int = 10
var ai interface{} = &a
println(ai)
}

逃逸分析:

$ go tool compile -l -m escape.go

可以看到,对于局部变量 interface{} 类型变量转换,不管是赋值还是赴地址都没有变量逃逸。这里发生了什么其实和上一节的局部变量一样,就不过多分析了。

值得提的一点是,给 interface{} 传地址,结构体的 word 将指向地址,而给 interface{} 传值,结构体的 word 是一个指针,将指向值所在的内存地址。这里由于是局部变量,这个变量值 a 是在栈上分配的,结构体 word 指向的是栈上值所在的地址。

再改写示例代码 1:

var ai interface{}

func main() {
var a int = 10
ai = a
println(ai)
}

逃逸分析:

$ go tool compile -l -m escape.go
escape.go:9:5: a escapes to heap

示例代码 2:

var ai interface{}

func main() {
var a int = 10
ai = &a
println(ai)
}

逃逸分析:

$ go tool compile -l -m escape.go
escape.go:8:6: moved to heap: a

可以看到,对于全局变量 ai 不管是传值还是传地址,变量 a 都将逃逸到堆中。为什么会这样也好理解:interface{} 反射的结构体表示是指针 data: unsafe.Pointer

通过汇编代码看传值的例子:

$ go tool compile -N -S -l escape.go
CALL runtime.convT64(SB)

重点看 runtime.convT64 函数,该函数会在堆上分配内存。详细看这里,不在展开了。

1.3 反射

示例代码如下:

func main() {
var a int = 10
var ai interface{} = a
fmt.Println(ai)
}

逃逸分析:

$ go tool compile -l -m escape.go
escape.go:11:13: ... argument does not escape
escape.go:11:13: a escapes to heap

这里代码除了 fmt.Println 改动基本和 1.2 节代码一样,为什么这时候 a 就逃到堆上了呢?

原因肯定在于 fmt.Println 函数,查看函数我们发现代码会走到 escapes(i) 这里,escapes 的函数实现是:

// Dummy annotation marking that the value x escapes,
// for use in cases where the reflect code is so clever that
// the compiler cannot follow.
func escapes(x interface{}) {
if dummy.b {
dummy.x = x
}
} var dummy struct {
b bool
x interface{}
}

再解释这段实现之前,先看下为什么要用 escapes(i) 函数:

// TODO: Maybe allow contents of a Value to live on the stack.
// For now we make the contents always escape to the heap. It
// makes life easier in a few places (see chanrecv/mapassign
// comment below). comment:
Note: some of the noescape annotations below are technically a lie,
but safe in the context of this package. Functions like chansend
and mapassign don't escape the referent, but may escape anything
the referent points to (they do shallow copies of the referent).
It is safe in this package because the referent may only point
to something a Value may point to, and that is always in the
heap (due to the escapes() call in ValueOf).

说白了,不用 escapes() 会让编译器很麻烦,这里涉及到 noescape,详细了解可看这里

escapes 实际上是一种欺骗行为,欺骗编译器使得编译器将变量逃逸到堆中。怎么欺骗的呢?其实和结合上两节分析,基本能看出来了。

在变量 escapes(i) 到 escapes(x interface{}) 时发生了类型转换,将 i 转换为 interface{} 类型,实际做的就是 1.2 节描述的行为。然后,重点在 dummy.x == x,全局变量 dummy.x 会引用转换的接口 x,由于 dummy.x 是一个 interface{} 类型,其实质是一个指针,所以编译器会将 interface x 中 data 指向的变量 i 分配到堆中。这里注意 i 可以是值也可以是地址,如果是地址,编译器会将地址指向的值分配到堆中。

可能描述起来较为复杂,复杂的原因是 interface{} 做了好几层包装。我们拆开包装,用一种简化方式看代码的欺骗行为:

var a *int

func main() {
x := 10 var f bool
if f {
a = &x
}
}

逃逸分析:

$ go tool compile -l -m escape.go
escape.go:33:2: moved to heap: x

可以看到,骗过了编译器使得变量 x 逃逸到了堆上,虽然 a = &x 不会执行。

1.4 总结

本篇文章通过几个逃逸分析案例重点分析 escapes 函数是如果做到欺骗编译器实现变量逃逸的。


go 变量逃逸分析的更多相关文章

  1. Go变量逃逸分析

    目录 什么是逃逸分析 为什么要逃逸分析 逃逸分析是怎么完成的 逃逸分析实例 总结 写过C/C++的同学都知道,调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程 ...

  2. JVM中启用逃逸分析

    -XX:+DoEscapeAnalysis 逃逸分析优化JVM原理我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量 ...

  3. JVM笔记-逃逸分析

    参考: http://www.iteye.com/topic/473355http://blog.sina.com.cn/s/blog_4b6047bc01000avq.html 什么是逃逸分析(Es ...

  4. JVM逃逸分析

    开启逃逸分析: -server -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m 关闭逃逸分析: -server -XX:-DoEsca ...

  5. 逃逸分析(Escape Analysis)

    一.什么是逃逸 逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到:这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量 ...

  6. golang逃逸分析和竞争检测

    最近在线上发现一块代码逻辑在执行N次耗时波动很大1ms~800ms,最开始以为是gc的问题,对代码进行逃逸分析,看哪些变量被分配到堆上了,后来发现是并发编程时对一个切片并发的写,导致存在竞争,类似下面 ...

  7. 深入理解Java中的逃逸分析

    在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译,第一段是把.java文件转换成.class文件.第二段编译是把.class转换成机器指令的过程. ...

  8. JVM的逃逸分析

    我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存.如果堆中对象数量太多,回收对象还有整理内存,都会会带来时间上的消耗, ...

  9. Java之JVM逃逸分析

    引言: 逃逸分析(Escape Analysis)是众多JVM技术中的一个使用不多的技术点,本文将通过一个实例来分析其使用场景. 概念 逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配 ...

  10. java虚拟机的逃逸分析

    逃逸分析作为其他优化手段提供依据的分析技术,其基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸.甚至还有可能被外部线程 ...

随机推荐

  1. 华企盾DSC服务器无法启动常见处理方法

    先查看<服务问题判断>文档.常见的 1.授权已经过期--需延长授权 2.ERR_BASE64 – 机器码变更 3.不能在该计算机上使用该数据库,需要解锁才可以--打开服务器配置解锁数据库 ...

  2. ASR项目实战-产品分析

    分析Google.讯飞.百度.阿里.QQ.搜狗等大厂的ASR服务,可以罗列出一款ASR服务所需要具备的能力. 产品分类 ASR云服务产品,从用户体验.时效性.音频时长,可以划分为如下几类: 实时短音频 ...

  3. 从零玩转第三方登录之WeChat公众号扫码关注登陆 -wechatgzh

    title: 从零玩转第三方登录之WeChat公众号扫码关注登陆 date: 2022-09-27 22:46:53.362 updated: 2023-03-30 13:28:41.359 url: ...

  4. SAM适配下游任务的探究:SAM Adapter

    本文分享自华为云社区<SAM适配下游任务的探究:SAM Adapter>,作者:Hint. 近期大模型的涌现给AI研究带来显著的发展,META的工作Segment Anything(SAM ...

  5. AUC/ROC:面试中80%都会问的知识点

    摘要:ROC/AUC作为机器学习的评估指标非常重要,也是面试中经常出现的问题(80%都会问到) 本文分享自华为云社区<技术干货 | 解决面试中80%问题,基于MindSpore实现AUC/ROC ...

  6. 推理实践丨如何使用MindStudio进行Pytorch模型离线推理

    摘要:本教程所示例的任务是Ascend Pytorch离线推理,即对给定的已经训练好的模型参数和推理脚本,在Ascend 310/710和Tesla设备上进行推理应用. 本文分享自华为云社区<使 ...

  7. “pip不是内部或外部命令,也不是可运行的程序或批处理文件” 到底有多么神秘

    摘要:pip不是内部或外部命令,也不是可运行的程序或批处理文件到底有多么神秘? 本文分享自华为云社区<揭开「pip不是内部或外部命令,也不是可运行的程序或批处理文件」的神秘面纱>,作者:A ...

  8. 云小课|打造企业数据“高内聚,低耦合”--试试GaussDB(DWS)逻辑集群,实现数据物理隔离

    阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说).深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云.更多精彩内容请单击此处. 摘要:逻辑集群是基于No ...

  9. 通过windows自带管理工具、系统命令行、快捷键等快速操作

    windows自带管理工具 我们win+R 输入一些命令,可以快速打开一些界面,比如: sysdm.cpl win10.win11 我电脑,属性与之前win7不同了,我希望打开之前的属性打不开了 通过 ...

  10. MQTT(EMQX) - Linux CentOS Docker 安装

    MQTT(EMQX) - Linux CentOS 直接安装 和 Docker 安装 常规安装 下载文件 版本选择:https://www.emqx.com/zh/downloads/broker/ ...