千家信息网

Go 语言变量分配到哪里了

发表于:2024-11-30 作者:千家信息网编辑
千家信息网最后更新 2024年11月30日,这篇文章主要介绍"Go 语言变量分配到哪里了",在日常操作中,相信很多人在Go 语言变量分配到哪里了问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"Go 语言变量分配到哪
千家信息网最后更新 2024年11月30日Go 语言变量分配到哪里了

这篇文章主要介绍"Go 语言变量分配到哪里了",在日常操作中,相信很多人在Go 语言变量分配到哪里了问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"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

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

到此,关于"Go 语言变量分配到哪里了"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

分配 变量 分析 编译 对象 就是 案例 编译器 问题 命令 方法 语言 参数 指针 学习 观察 内存 类型 结果 也就是 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 亳州人在杭州做软件开发 我的世界服务器2b2t的地址 双十一云服务器图片 郑州我游网络技术 香港网络安全月 合肥众诚软件开发 服务器不进入系统怎么格式化硬盘 国家网络安全保卫局 cad材料数据库加载失败 数据库安全操作 实验报告 外模式可以提高数据库安全性 苹果无法安全连接服务器 以下属于商情数据库的是 网络安全知识小课堂内容 海南服务器包装公司报价 连云港手机软件开发 人大金仓和哪个数据库相似 数据库哪些模糊语句 软件开发创业需要的人才 笔记本穿越火线服务器连接失败 服务器为什么要管理控制面板 百瑞傲达软件如何导入数据库 河北c语言软件开发哪家好 大豆参考基因组数据库版本不一样 网络安全班会课感想 企业网络安全解决文案 可以免费使用的云服务器 虹口区新时代软件开发品牌 网络技术弱电 杭州类似抖音的软件开发
0