GopherCon SG 2019 "Understanding Allocations" 学习笔记

本篇是根据 GopherCon SG 2019 “Understanding Allocations” 演讲的学习笔记。

Understanding Allocations: the Stack and the Heap - GopherCon SG 2019 - YouTube


理解分配:栈和堆

image-20220402085721628

你的程序中有两种内存,栈内存和堆内存。

go 中,每个 go 程都会有一个栈空间,整个程序有一个堆空间。


变量是在栈还是堆上

image-20220402090134921

负责堆垃圾回收的 GC 会导致整个程序的延迟,而不仅仅是创建垃圾的部分。你可能会担心你的代码在堆中产生了多少垃圾。


什么时候需要优化

image-20220402090405261

要有 benchmarks 基准来证明你的程序不够快(有大量的堆内存分配),够快就不用多此一举了。

你要先确保程序能正确运作(业务处理),而不是先着重性能优化。

在有大量指针+运行快速的程序、清晰明了+但有点慢的程序之间,你应该选择后者。


普通类型参数传递

第5行进入squre函数

image-20220402090854892

函数和局部变量被挤压入栈,一个函数为一个堆栈帧


squre函数执行完

image-20220402091022503

执行完成后,你会发现黑线(只是用于区分)向上移,上方内容为有效内容,下方内容为无效内容(过期而不再使用


第6行执行println函数

image-20220402091208725

go 声明了新的内存部分,我们有了新的堆栈帧用于打印行,黑线下移。

通俗的来讲,栈空间会进行自我清理,任何变量都会被清理干净,空间会被重复使用。


指针类型参数传递

第4行声明变量

image-20220402091731899

main 函数和变量 n 被压入栈。


第5行进入inc函数

image-20220402091848706

inc 函数和变量 x 被压入栈,属于另一个堆栈帧,黑线下移。传入的变量是 n 的地址。


inc函数执行完

image-20220402091940275

黑线上移,n 被修改了值。并没有什么问题。


执行println函数

image-20220402092013903


总结

虽然使用了指针传参,但这种情况下它能够留在栈上,即共享向下传递时,通常留在栈空间上。(主函数将自己的变量传给子函数)


函数返回引用

第4行进入answer函数前

image-20220402092408388

编译器推断出 n 是 int 的指针类型,赋初值 nil 等待函数返回结果。


进入函数

image-20220402092816889

在函数内部声明了变量并初始化,并返回了变量的内存地址。


执行println函数

image-20220402092908776

致命的问题

你会发现,我明明没有进行赋值修改的操作,但却破坏了原有的值。原因在于

  • 调用 answer 函数的时候,假如我们把声明的变量 x 放在栈空间,返回变量的地址。
  • 那么 main 函数中的 n 会拿到 x 的地址,当 answer 方法退出时(黑线上移),该堆栈帧已过期,但是 n 还引用着 x
  • 我们知道堆栈的空间是可以被重复利用的,那么下一次执行其他函数或者声明变量的时候,那块过期的内存区域就会被重新使用,修改为其他值。
  • 案例中调用 println,黑线下移,产生新的堆栈帧,原先 x 标识符被替换成了 a,修改 a 的值等同修改了 n 地址解引用后的值。这是非常致命的错误。

解决的方法

image-20220402093057260

为了解决上述致命的问题,被声明初始化的 x 变量就需要“逃逸”到堆空间中。这样其他无关函数就不会对它的值产生影响。

共享向上传递时,通常留在堆空间上。(子函数将自己的变量传给主函数,引用)

Escape Analysis

编译器怎么判断的

image-20220402162719312

image-20220402162815103

变量只在函数里工作,那就分配到栈空间上。如果编译器不能证明函数返回后声明过的变量是否被引用,那必须将其分配到堆空间上。


询问编译器

image-20220402162929960

查看构建指令,可以提供一个 -gcflags 的可选参数,传递给 go tool compile 工具

image-20220402163001136

查询 go tool compile -h,得知参数 -m 可以输出编译器的优化决定(变量放在栈还是堆上)

image-20220402163016040

执行 go build -gcflags "-m" 获取到下面的结果

image-20220402163024090

image-20220402163050766

总结

image-20220402164009984

image-20220402164149835

什么时候会把变量分配在堆内存上

  • 函数返回退出后声明过的变量依旧被引用着。
  • 变量初始化值大小过大无法分配进栈。
  • 不知道变量值的具体大小,比如切片。

做个判断

image-20220402164217660

看看前面总结的第一点和第三点。明显可以知道右边那个是分配在栈上,而左边那个分配在堆上。


思考io.Reader接口

image-20220402164256481

io 标准库里有个 Reader 接口,你应该知道为什么官方要用前者替换后者了吧。如果使用后者,会在堆上产生大量的垃圾,造成程序迟钝。前者则符合向下传递思想,变量通常分配在栈空间上。


最后

image-20220402164503442

不要猜想,多用工具!

posted @ 2022-04-03 01:54  小能日记  阅读(14)  评论(0编辑  收藏  举报