Go 语言变量分配到哪里了

问题

type User struct {  ID     int64  Name   string  Avatar string }  func GetUserInfo() *User {  return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"} }  func main() {  _ = GetUserInfo() }

开局就是一把问号,带着问题进行学习。请问 main 调用 GetUserInfo  后返回的&User{…}。这个变量是分配到栈上了呢,还是分配到堆上了?

什么是堆/栈

在这里并不打算详细介绍堆栈,仅简单介绍本文所需的基础知识。如下:

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

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

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

什么是逃逸分析

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

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

是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上。

即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上。

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

在什么阶段确立逃逸

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

为什么需要逃逸

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

  • 垃圾回收(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 查看所有允许传递给编译器的标识参数。

逃逸案例

案例一:指针

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

type User struct {  ID     int64  Name   string  Avatar string }  func GetUserInfo() *User {  return &User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"} }  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  ...  0x0028 00040 (main.go:10) MOVQ AX, (SP)  0x002c 00044 (main.go:10) CALL runtime.newobject(SB)  0x0031 00049 (main.go:10) PCDATA $2, $1  0x0031 00049 (main.go:10) MOVQ 8(SP), AX  0x0036 00054 (main.go:10) MOVQ $13746731, (AX)  0x003d 00061 (main.go:10) MOVQ $7, 16(AX)  0x0045 00069 (main.go:10) PCDATA $2, $-2  0x0045 00069 (main.go:10) PCDATA $0, $-2  0x0045 00069 (main.go:10) CMPL runtime.writeBarrier(SB), $0  0x004c 00076 (main.go:10) JNE 156  0x004e 00078 (main.go:10) LEAQ go.string."EDDYCJY"(SB), CX     ...

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

分析结果

这是因为 GetUserInfo() 返回的是指针对象,引用被返回到了方法之外了。因此编译器会把该对象分配到堆上,而不是栈上。

否则方法结束之后,局部变量就被回收了,岂不是翻车。所以最终分配到堆上是理所当然的

再想想

那你可能会想,那就是所有指针对象,都应该在堆上?并不。如下:

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

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

$ 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 = "EDDYCJY"   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  类型会导致该对象分配到堆上。

案例三、泄露参数

type User struct {  ID     int64  Name   string  Avatar string }  func GetUserInfo(u *User) *User {  return u }  func main() {  _ = GetUserInfo(&User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}) }

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

$ 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() 之中,所以分配在栈上。

再想想

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

type User struct {  ID     int64  Name   string  Avatar string }  func GetUserInfo(u User) *User {  return &u }  func main() {  _ = GetUserInfo(User{ID: 13746731, Name: "EDDYCJY", Avatar: "https://avatars0.githubusercontent.com/u/13746731"}) }

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

$ 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

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

阅读剩余 70%

原创文章,作者:GPKQX,如若转载,请注明出处:https://www.beidanyezhu.com/a/29157.html

(0)
GPKQX的头像GPKQX
上一篇 2025-02-05
下一篇 2025-02-05

相关推荐

  • 第7期(语言优美的句子)

    2022-03-12 星期六 虎 壬寅年 二月初十 【语录集锦】 亲情 满地都是金黄的梧桐叶,街上没有多少行人,你我走在回家的路上,只不过没有你,我想你了。 友情有一种朋友,距离再…

  • 爱孩子的五种语言(对儿女的爱的句子)

    查普曼博士提出了五种爱的语言——肯定的言辞、精心的时刻、接受礼物、服务的行动和身体的接触。这本书里主要是用于夫妻之间的爱语,其实家长和孩子的也是这样的五种爱语。 著名婚姻家庭专家盖…

  • 语言发育迟缓康复干货(语言发育迟缓怎么做康复训练)

    家有语言发育迟缓宝宝,最难的是什么吗? 语言发育迟缓宝宝训练有多难!最难的就是不能说话,不理解别人说话,早期发现时,多处于2-3岁,且行为能力有限,多数具有智力障碍和社交障碍,很多…

  • 语言(逅怎么读)

    引 言吴语是汉语族的一个重要分支,分布在江苏南部、浙江省大部、上海市全境,安徽南部及福建、江西的小部分地区。学术界将其分爲太湖片、台州片、东瓯片、婺州片、处衢片、宣州片六个小片。其…

  • 语言的谋略(洛邑怎么读)

    第十七辑 国家政策辩论的谋略与技巧文/钟百超 每个人对一定事物或做法都有自己的主见,而这个主见的形成与个人的信仰、理念、知识、修养,乃至利益都有密切相关。一个人能否提出一个有利于国…

  • 怎么在Go语言中隐藏窗口

    获取窗口句柄 在操作窗口之前,需要先获取窗口的句柄。在Windows平台上,每个窗口都有一个唯一的句柄用于标识该窗口。可以使用Windows API函数FindWindow或者Fi…

  • 怎么使用Go语言实现时间轮

    时间轮概述 时间轮是一种基于时间概念的循环缓冲区,可以将其视为一个圆形的缓冲区,其大小为m(2的幂次)。每次时间轮转动一个单位,例如1毫秒,所有缓冲区指向的内容也随之发生改变。在时…

  • Go语言怎么删除MongoDB中的文档

    一、删除文档 删除单个文档 首先,我们来看如何使用mgo删除单个文档。 session, err := mgo.Dial("localhos…

  • 怎么在Ubuntu系统中安装Go语言

    下载Go语言 Go语言的安装包可以在官方网站上下载。在下载之前,您应该确认需要的系统版本和Go语言的版本。您可以按照以下命令查看您正在使用的Ubuntu版本: $ lsb…

  • Go语言中除法运算的效率怎么提高

    近年来,Go语言在程序员中的人气越来越高,在高并发场景下的优势尤为突出。作为一门强调高效率的语言,Go语言的性能一直 在计算机科学中,除法运算是一种非常常见的运算。在Go语言中,最…

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

分享本页
返回顶部