疑问

请问main调用GetUserInfo后返回的&User{...}。这个变量是分配到栈上了呢,还是分配到堆上了?

package main

type User struct {
ID int64
Name string
} func GetUserInfo() *User {
return &User{ID: 1, Name: "niuben"}
} func main() {
_ = GetUserInfo()
}

什么是堆、栈

堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多。

栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上。

Go 语言,它的堆栈分配是通过 Compiler 进行分析,GC 去管理的,而对其的分析选择动作就是今天探讨的重点。

什么是逃逸分析

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。

通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:

  1. 是否有在其他地方(非局部)被引用。只有有可能被引用了,那么它一定分配到堆上。否则分配到栈上。
  2. 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上。

对此可以理解为,逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为

在什么阶段确定逃逸

在编译阶段确定逃逸,注意并不是在运行时。

为什么需要逃逸

这个问题我们可以反过来想,如果变量都分配到堆上了会出现什么事情?例如:

  • 垃圾回收(GC)的压力不断增大。
  • 申请、分配、回收内存的系统开销增大(相对于栈)。
  • 动态分配产生一定量的内存碎片。

简单来说,就是频繁申请并分配堆内存是有一定“代价”的。会影响应用程序运行的效率,间接影响到整体系统。

因此,“按需分配”最大限度的灵活利用资源,才是正确的治理之道。这也就是为什么需要逃逸分析的原因。

怎么确定是否逃逸

第一,通过编译器命令,就可以看到详细的逃逸分析过程。而指令集 -gcflags 用于将标识参数传递给 Go 编译器,涉及如下:

  • -m 会打印出逃逸分析的优化策略,实际上最多总共可以用4个 -m,但是信息量较大,一般用1个就可以了。
  • -l 会禁用函数内联,在这里禁用掉 inline 能更好的观察逃逸情况,减少干扰。
$ go build -gcflags '-m -l' main.go

第二,通过反编译命令查看

$ go tool compile -S main.go

注:可以通过 go tool compile -help 查看所有允许传递给编译器的标识参数。

逃逸案例

案例一:指针

第一个案例是一开始抛出的问题,现在你再想想看,如下:

package main

type User struct {
ID int64
Name string
} func GetUserInfo() *User {
return &User{ID: 1, Name: "niuben"}
} func main() {
_ = GetUserInfo()
}

执行命令观察一下,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:54: &User literal escapes to heap

通过查看分析结果,可得知 &User 逃到了堆里,也就是分配到堆上了。这是不是有问题呀... 再看看汇编代码确定一下,如下:

$ go tool compile -S main.go
"".GetUserInfo STEXT size=190 args=0x8 locals=0x18
0x0000 00000 (main.go:9) TEXT "".GetUserInfo(SB), $24-8
...
0x0013 00019 (test10.go:8) MOVQ BP, 16(SP)
0x0018 00024 (test10.go:8) LEAQ 16(SP), BP
0x001d 00029 (test10.go:8) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x001d 00029 (test10.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (test10.go:9) LEAQ type."".User(SB), AX
0x0024 00036 (test10.go:9) MOVQ AX, (SP)
0x0028 00040 (test10.go:9) PCDATA $1, $0
0x0028 00040 (test10.go:9) CALL runtime.newobject(SB)
0x002d 00045 (test10.go:9) MOVQ 8(SP), AX
0x0032 00050 (test10.go:9) MOVQ $1, (AX)
0x0039 00057 (test10.go:9) MOVQ $6, 16(AX)
0x0041 00065 (test10.go:9) LEAQ go.string."niuben"(SB), CX
...

我们将目光集中到CALL指令,发现其执行了 runtime.newobject 方法,也就是确实是分配到了堆上。这是为什么呢?

分析结果

这是因为 GetUserInfo() 返回的是指针对象,就应该在堆上?并不。如下:

func main() {
str := new(string)
*str = "niuben"

你想想这个对象会分配到哪里?如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:4:12: main new(string) does not escape

显然,该对象分配到栈上了。很核心的一点就是它有没有被作用域之外所引用,而这里作用域仍然保留在 main 中,因此它没有发生逃逸。

案例二:未确定类型

func main() {
str := new(string)
*str = "niuben" fmt.Println(str)
}

执行命令观察一下,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:13: str escapes to heap
./main.go:6:12: new(string) escapes to heap
./main.go:9:13: main ... argument does not escape

通过查看分析结果,可得知 str 变量逃到了堆上,也就是该对象在堆上分配。但上个案例时它还在栈上,我们也就 fmt 输出了它而已。这...到底发生了什么事?

分析结果

相对案例一,案例二只加了一行代码 fmt.Println(str) ,问题肯定出现在它身上。其原型:

func Println(a ...interface{})(n int, err error)

通过对其分析,可得知当形参为 interface 类型时,在编译阶段编译器无法确定其具体的类型。因此会产生逃逸,最终分配到堆上。

如果你有兴趣追源码的话,可以看下内部的 reflect.TypeOf(arg).Kind() 语句,其会造成堆逃逸,而表象就是 interface 类型会导致该对象分配到堆上。

案例三:泄露参数

package main

type User struct {
ID int64
Name string
} func GetUserInfo(u *User) *User {
return u
} func main() {
_ = GetUserInfo(&User{ID: 1, Name: "niuben"})
}

执行命令观察一下,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:9:18: leaking param: u to result ~r1 level=0
./main.go:14:63: main &User literal does not escape

我们注意到 leaking param 的表述,它说明了变量 u 是一个泄露参数。结合代码可得知其传给 GetUserInfo 方法后,没有做任何引用之类的涉及变量的动作,直接就把这个变量返回出去了。

因此这个变量实际上并没有逃逸,它的作用域还在 main() 之中,所以分配在栈上。

再想想

那你再想想怎样才能让它分配到堆上?结合案例一,举一反三。

package main

type User struct {
ID int64
Name string
} func GetUserInfo(u User) *User {
return &u
} func main() {
_ = GetUserInfo(User{ID: 1, Name: "niuben"})
}

执行命令观察一下,如下:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:10:9: &u escapes to heap
./main.go:9:18: moved to heap: u

只要一小改,它就考虑会被外部所引用,因此妥妥的分配到堆上了

总结

在本文我给你介绍了逃逸分析的概念和规则,并列举了一些例子加深理解。但实际肯定远远不止这些案例,你需要做到的是掌握方法,遇到再看就好了。除此之外你还需要注意:

  • 静态分配到栈上,性能一定比动态分配到堆上好。
  • 底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心。
  • 每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)。
  • 直接通过 go build -gcflags '-m -l' 就可以看到逃逸分析的过程和结果。
  • 到处都用指针传递并不一定是最好的,要用对。

这块的知识点。我的建议是适当了解,但没必要硬记,因为 Go 语言每次升级都有可能会改。靠基础知识点加命令调试观察就好了。

参考

golang 逃逸分析详解的更多相关文章

  1. Memcache的使用和协议分析详解

    Memcache的使用和协议分析详解 作者:heiyeluren博客:http://blog.csdn.NET/heiyeshuwu时间:2006-11-12关键字:PHP Memcache Linu ...

  2. wav文件格式分析详解

    wav文件格式分析详解 文章转载自:http://blog.csdn.net/BlueSoal/article/details/932395 一.综述    WAVE文件作为多媒体中使用的声波文件格式 ...

  3. 线程组ThreadGroup分析详解 多线程中篇(三)

    线程组,顾名思义,就是线程的组,逻辑类似项目组,用于管理项目成员,线程组就是用来管理线程. 每个线程都会有一个线程组,如果没有设置将会有些默认的初始化设置 而在java中线程组则是使用类ThreadG ...

  4. HanLP中人名识别分析详解

    HanLP中人名识别分析详解 在看源码之前,先看几遍论文<基于角色标注的中国人名自动识别研究> 关于命名识别的一些问题,可参考下列一些issue: l ·名字识别的问题 #387 l ·机 ...

  5. Golang Context 包详解

    Golang Context 包详解 0. 引言 在 Go 语言编写的服务器程序中,服务器通常要为每个 HTTP 请求创建一个 goroutine 以并发地处理业务.同时,这个 goroutine 也 ...

  6. Golang逃逸分析

    Golang逃逸分析 介绍逃逸分析的概念,go怎么开启逃逸分析的log. 以下资料来自互联网,有错误之处,请一定告之. sheepbao 2017.06.10 什么是逃逸分析 wiki上的定义 In ...

  7. GC日志分析详解

    点击返回上层目录 原创声明:作者:Arnold.zhao 博客园地址:https://www.cnblogs.com/zh94 GC日志分析详解 以ParallelGC为例,YoungGC日志解释如下 ...

  8. Go For Web:Golang http 包详解(源码剖析)

    前言: 本文作为解决如何通过 Golang 来编写 Web 应用这个问题的前瞻,对 Golang 中的 Web 基础部分进行一个简单的介绍.目前 Go 拥有成熟的 Http 处理包,所以我们去编写一个 ...

  9. HashMap实现原理分析(详解)

    1. HashMap的数据结构 http://blog.csdn.net/gaopu12345/article/details/50831631   ??看一下 数据结构中有数组和链表来实现对数据的存 ...

  10. MongoDB执行计划分析详解

    要保证数据库处于高效.稳定的状态,除了良好的硬件基础.高效高可用的数据库架构.贴合业务的数据模型之外,高效的查询语句也是不可少的.那么,如何查看并判断我们的执行计划呢?我们今天就来谈论下MongoDB ...

随机推荐

  1. Qt/C++编写监控实时显示和取流回放工具(回放支持切换进度)

    一.前言 现在各个监控大厂做的设备,基本上都会支持通过rtsp直接取流显示,而且做的比较好的还支持通过rtsp回放取流,基本上都会约定一个字符串的规则,每个厂家都是不一样的规则,比如回放对应的rtsp ...

  2. WebSocket从入门到精通,半小时就够!

    本文原题"WebSocket:5分钟从入门到精通",作者"程序猿小卡_casper",原文链接见文末参考资料部分.本次收录时有改动. 1.引言 自从HTML5里 ...

  3. 为什么 .NET8线程池 容易引发线程饥饿

    一:背景 1. 讲故事 最近时间相对比较宽裕,多写点文章来充实社区吧,这篇文章主要还是来自于最近遇到的几例线程饥饿(Task.Result)引发的一系列的反思和总结,我觉得.NET8容易引发饥饿的原因 ...

  4. 前端学习openLayers配合vue3(图层中心点的偏移)

    有了上一步的学习,我们知道了如何创建一个地图,现在我们来尝试更改一下图层的中心点 关键代码 let view = map.getView();//获取视图层 let center = view.get ...

  5. C#/.NET/.NET Core技术前沿周刊 | 第 20 期(2025年1.1-1.5)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  6. 2021 年万圣节 Github 彩蛋

    记录每年 Github 万圣节彩蛋,也记录有来项目成长历程. 2021 万圣节彩蛋 2020 万圣节彩蛋

  7. 了解 ASP.NET Core 中的中间件

    在 .NET Core 中,中间件(Middleware) 是处理 HTTP 请求和响应的核心组件.它们被组织成一个请求处理管道,每个中间件都可以在请求到达最终处理程序之前或之后执行操作.中间件可以用 ...

  8. Solution Set -「AGC 001~003」C~F

    目录 「AGC 001C」Shorten Diameter 「AGC 001D」Arrays and Palindrome * 「AGC 001E」BBQ Hard * 「AGC 001F」Wild ...

  9. c# WindowsCommunityToolkit--- Shade Animation

    WindowsCommunityToolkit: https://github.com/CommunityToolkit/WindowsCommunityToolkit You can also pr ...

  10. 一款基于 WPF 开源、功能全面的串口调试工具

    前言 今天大姚给大家分享一款基于 WPF 开源(MIT License).免费.功能全面的串口调试工具:BYSerial. 项目介绍 BYSerial是一款基于 WPF 开源(MIT License) ...