Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
go开源项目镜像
GolangCodingTime
提交
290814a6
G
GolangCodingTime
项目概览
go开源项目镜像
/
GolangCodingTime
通知
0
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
G
GolangCodingTime
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
290814a6
编写于
8月 29, 2021
作者:
写代码的明哥
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
补充逃逸分析相关文章
上级
71c9ed8f
变更
4
隐藏空白更改
内联
并排
Showing
4 changed file
with
693 addition
and
0 deletion
+693
-0
source/c07/c07_07.md
source/c07/c07_07.md
+34
-0
source/c07/c07_07.rst
source/c07/c07_07.rst
+40
-0
source/c08/c08_03.md
source/c08/c08_03.md
+285
-0
source/c08/c08_03.rst
source/c08/c08_03.rst
+334
-0
未找到文件。
source/c07/c07_07.md
0 → 100644
浏览文件 @
290814a6
# 7.7 局部变量分配在栈上还是堆上?
## 什么是堆内存和栈内存?
根据内存管理(分配和回收)方式的不同,可以将内存分为
**堆内存**
和
**栈内存**
。
那么他们有什么区别呢?
**堆内存**
:由内存分配器和垃圾收集器负责回收
**栈内存**
:由编译器自动进行分配和释放
一个程序运行过程中,也许会有多个栈内存,但肯定只会有一个堆内存。
每个栈内存都是由线程或者协程独立占有,因此从栈中分配内存不需要加锁,并且栈内存在函数结束后会自动回收,性能相对堆内存好要高。
而堆内存呢?由于多个线程或者协程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁,避免造成冲突,并且堆内存在函数结束后,需要 GC (垃圾回收)的介入参与,如果有大量的 GC 操作,将会吏程序性能下降得历害。
## 局部变量是从哪里分配的?
在函数里声明定义的变量,我们称之为局部变量。
一般来说,局部变量的作用域仅在该函数中,当函数返回后,所有局部变量所占用的内存空间都将被收回,对于这类变量,都是从栈上分配内存空间,这一点大家应该是没有争议的。
可有一种局部变量,比较特殊。
这种局部变量,虽然在函数里声明定义,但是在函数外还会持续的使用。
对于这类局部变量,显然我们是不希望函数退出后将其销毁的。
那怎么办呢?可以从堆区分配内存空间给这类局部变量。
不过这个事实其实不用程序员操心,Go 的编译器会自行判断做优化的。但我们仍然需要知道这个知识点(因为面试会问哈哈)
source/c07/c07_07.rst
0 → 100644
浏览文件 @
290814a6
7.7 局部变量分配在栈上还是堆上?
================================
什么是堆内存和栈内存?
----------------------
根据内存管理(分配和回收)方式的不同,可以将内存分为 **堆内存** 和
**栈内存**\ 。
那么他们有什么区别呢?
**堆内存**\ :由内存分配器和垃圾收集器负责回收
**栈内存**\ :由编译器自动进行分配和释放
一个程序运行过程中,也许会有多个栈内存,但肯定只会有一个堆内存。
每个栈内存都是由线程或者协程独立占有,因此从栈中分配内存不需要加锁,并且栈内存在函数结束后会自动回收,性能相对堆内存好要高。
而堆内存呢?由于多个线程或者协程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁,避免造成冲突,并且堆内存在函数结束后,需要
GC (垃圾回收)的介入参与,如果有大量的 GC
操作,将会吏程序性能下降得历害。
局部变量是从哪里分配的?
------------------------
在函数里声明定义的变量,我们称之为局部变量。
一般来说,局部变量的作用域仅在该函数中,当函数返回后,所有局部变量所占用的内存空间都将被收回,对于这类变量,都是从栈上分配内存空间,这一点大家应该是没有争议的。
可有一种局部变量,比较特殊。
这种局部变量,虽然在函数里声明定义,但是在函数外还会持续的使用。
对于这类局部变量,显然我们是不希望函数退出后将其销毁的。
那怎么办呢?可以从堆区分配内存空间给这类局部变量。
不过这个事实其实不用程序员操心,Go
的编译器会自行判断做优化的。但我们仍然需要知道这个知识点(因为面试会问哈哈)
source/c08/c08_03.md
0 → 100644
浏览文件 @
290814a6
# 8.3 Go 语言中的内存分配规律及逃逸分析
## 1. 分配内存三大组件
Go 分配内存的过程,主要由三大组件所管理,级别从上到下分别是:
### mheap
Go 在程序启动时,首先会向操作系统申请一大块内存,并交由
`mheap`
结构全局管理。
具体怎么管理呢? mheap 会将这一大块内存,切分成不同规格的小内存块,我们称之为 mspan,根据规格大小不同,mspan 大概有 70类左右,划分得可谓是非常的精细,足以满足各种对象内存的分配。
那么这些 mspan 大大小小的规格,杂乱在一起,肯定很难管理对吧?
因此就有了 mcentral 这下一级组件
### mcentral
启动一个 Go 程序,会初始化很多的 mcentral ,每个 mcentral 只负责管理一种特定规格的 mspan。
相当于 mcentral 实现了在 mheap 的基础上对 mspan 的精细化管理。
但是 mcentral 在 Go 程序中是全局可见的,因此如果每次协程来 mcentral 申请内存的时候,都需要加锁。
可以预想,如果每个协程都来 mcentral 申请内存,那频繁的加锁释放锁开销是非常大的。
因此需要有一个 mcentral 的二级代理来缓冲这种压力
### mcache
在一个 Go 程序里,每个线程
`M`
会绑定给一个处理器
`P`
,在单一粒度的时间里只能做多处理运行一个
`goroutine`
,每个
`P`
都会绑定一个叫
`mcache`
的本地缓存。
当需要进行内存分配时,当前运行的
`goroutine`
会从
`mcache`
中查找可用的
`mspan`
。从本地
`mcache`
里分配内存时不需要加锁,这种分配策略效率更高。
### mspan 供应链
mcache 的 mspan 数量并不总是充足的,当供不应求的时候,mcache 会从 mcentral 再次申请更多的 mspan,同样的,如果 mcentral 的 mspan 数量也不够的话,mcentral 也会向它的上级 mheap 申请 mspan。再极端一点,如果 mheap 里的 mspan 也无法满足程序的内存申请,那该怎么办?
那就没办法啦,mheap 只能厚着脸皮跟操作系统这个老大哥申请了。
以上的供应流程,只适用于内存块小于 64KB 的场景,原因在于Go 没法使用工作线程的本地缓存
`mcache`
和全局中心缓存
`mcentral`
上管理超过 64KB 的内存分配,所以对于那些超过 64KB 的内存申请,会直接从堆上(
`mheap`
)上分配对应的数量的内存页(每页大小是 8KB)给程序。
## 2. 什么是堆内存和栈内存?
根据内存管理(分配和回收)方式的不同,可以将内存分为
**堆内存**
和
**栈内存**
。
那么他们有什么区别呢?
**堆内存**
:由内存分配器和垃圾收集器负责回收
**栈内存**
:由编译器自动进行分配和释放
一个程序运行过程中,也许会有多个栈内存,但肯定只会有一个堆内存。
每个栈内存都是由线程或者协程独立占有,因此从栈中分配内存不需要加锁,并且栈内存在函数结束后会自动回收,性能相对堆内存好要高。
而堆内存呢?由于多个线程或者协程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁,避免造成冲突,并且堆内存在函数结束后,需要 GC (垃圾回收)的介入参与,如果有大量的 GC 操作,将会吏程序性能下降得历害。
## 3. 逃逸分析的必要性
由此可以看出,为了提高程序的性能,应当尽量减少内存在堆上分配,这样就能减少 GC 的压力。
在判断一个变量是在堆上分配内存还是在栈上分配内存,虽然已经有前人已经总结了一些规律,但依靠程序员能够在编码的时候时刻去注意这个问题,对程序员的要求相当之高。
好在 Go 的编译器,也开放了逃逸分析的功能,使用逃逸分析,可以直接检测出你程序员所有分配在堆上的变量(这种现象,即是逃逸)。
方法是执行如下命令
```
shell
go build
-gcflags
'-m -l'
demo.go
# 或者再加个 -m 查看更详细信息
go build
-gcflags
'-m -m -l'
demo.go
```
## 4. 内存分配位置的规律
如果逃逸分析工具,其实人工也可以判断到底有哪些变量是分配在堆上的。
那么这些规律是什么呢?
经过总结,主要有如下四种情况
1.
根据变量的使用范围
2.
根据变量类型是否确定
3.
根据变量的占用大小
4.
根据变量长度是否确定
接下来我们一个一个分析验证
### 根据变量的使用范围
当你进行编译的时候,编译器会做逃逸分析(escape analysis),当发现一个变量的使用范围仅在函数中,那么可以在栈上为它分配内存。
比如下边这个例子
```
go
func
foo
()
int
{
v
:=
1024
return
v
}
func
main
()
{
m
:=
foo
()
fmt
.
Println
(
m
)
}
```
我们可以通过
`go build -gcflags '-m -l' demo.go`
来查看逃逸分析的结果,其中
`-m`
是打印逃逸分析的信息,
`-l`
则是禁止内联优化。
从分析的结果我们并没有看到任何关于 v 变量的逃逸说明,说明其并没有逃逸,它是分配在栈上的。
```
go
$
go
build
-
gcflags
'
-
m
-
l
'
demo
.
go
#
command
-
line
-
arguments
./
demo
.
go
:
12
:
13
:
...
argument
does
not
escape
./
demo
.
go
:
12
:
13
:
m
escapes
to
heap
```
而如果该变量还需要在函数范围之外使用,如果还在栈上分配,那么当函数返回的时候,该变量指向的内存空间就会被回收,程序势必会报错,因此对于这种变量只能在堆上分配。
比如下边这个例子,
**返回的是指针**
```
go
func
foo
()
*
int
{
v
:=
1024
return
&
v
}
func
main
()
{
m
:=
foo
()
fmt
.
Println
(
*
m
)
// 1024
}
```
从逃逸分析的结果中可以看到
`moved to heap: v`
,v 变量是从堆上分配的内存,和上面的场景有着明显的区别。
```
go
$
go
build
-
gcflags
'
-
m
-
l
'
demo
.
go
#
command
-
line
-
arguments
./
demo
.
go
:
6
:
2
:
moved
to
heap
:
v
./
demo
.
go
:
12
:
13
:
...
argument
does
not
escape
./
demo
.
go
:
12
:
14
:
*
m
escapes
to
heap
```
除了返回指针之外,还有其他的几种情况也可归为一类:
**第一种情况:返回任意引用型的变量:Slice 和 Map**
```
go
func
foo
()
[]
int
{
a
:=
[]
int
{
1
,
2
,
3
}
return
a
}
func
main
()
{
b
:=
foo
()
fmt
.
Println
(
b
)
}
```
逃逸分析结果
```
go
$
go
build
-
gcflags
'
-
m
-
l
'
demo
.
go
#
command
-
line
-
arguments
./
demo
.
go
:
6
:
12
:
[]
int
literal
escapes
to
heap
./
demo
.
go
:
12
:
13
:
...
argument
does
not
escape
./
demo
.
go
:
12
:
13
:
b
escapes
to
heap
```
**第二种情况:在闭包函数中使用外部变量**
```
go
func
Increase
()
func
()
int
{
n
:=
0
return
func
()
int
{
n
++
return
n
}
}
func
main
()
{
in
:=
Increase
()
fmt
.
Println
(
in
())
// 1
fmt
.
Println
(
in
())
// 2
}
```
逃逸分析结果
```
go
$
go
build
-
gcflags
'
-
m
-
l
'
demo
.
go
#
command
-
line
-
arguments
./
demo
.
go
:
6
:
2
:
moved
to
heap
:
n
./
demo
.
go
:
7
:
9
:
func
literal
escapes
to
heap
./
demo
.
go
:
15
:
13
:
...
argument
does
not
escape
./
demo
.
go
:
15
:
16
:
in
()
escapes
to
heap
```
### 根据变量类型是否确定
在上边例子中,也许你发现了,所有编译输出的最后一行中都是
`m escapes to heap`
。
奇怪了,为什么 m 会逃逸到堆上?
其实就是因为我们调用了
`fmt.Println()`
函数,它的定义如下
```
go
func
Println
(
a
...
interface
{})
(
n
int
,
err
error
)
{
return
Fprintln
(
os
.
Stdout
,
a
...
)
}
```
可见其接收的参数类型是
`interface{}`
,对于这种编译期不能确定其参数的具体类型,编译器会将其分配于堆上。
### 根据变量的占用大小
最开始的时候,就介绍到,以 64KB 为分界线,我们将内存块分为 小内存块 和 大内存块。
小内存块走常规的 mspan 供应链申请,而大内存块则需要直接向 mheap,在堆区申请。
以下的例子来说明
```
go
func
foo
()
{
nums1
:=
make
([]
int
,
8191
)
// < 64KB
for
i
:=
0
;
i
<
8191
;
i
++
{
nums1
[
i
]
=
i
}
}
func
bar
()
{
nums2
:=
make
([]
int
,
8192
)
// = 64KB
for
i
:=
0
;
i
<
8192
;
i
++
{
nums2
[
i
]
=
i
}
}
```
给
`-gcflags`
多加个
`-m`
可以看到更详细的逃逸分析的结果
```
go
$
go
build
-
gcflags
'
-
m
-
l
'
demo
.
go
#
command
-
line
-
arguments
./
demo
.
go
:
5
:
15
:
make
([]
int
,
8191
)
does
not
escape
./
demo
.
go
:
12
:
15
:
make
([]
int
,
8192
)
escapes
to
heap
```
那为什么是 64 KB 呢?
我只能说是试出来的 (8191刚好不逃逸,8192刚好逃逸),网上有很多文章千篇一律的说和
`ulimit -a`
中的
`stack size`
有关,但经过了解这个值表示的是系统栈的最大限制是 8192 KB,刚好是 8M。
```
shell
$
ulimit
-a
-t
: cpu
time
(
seconds
)
unlimited
-f
: file size
(
blocks
)
unlimited
-d
: data seg size
(
kbytes
)
unlimited
-s
: stack size
(
kbytes
)
8192
```
我个人实在无法理解这个 8192 (8M) 和 64 KB 是如何对应上的,如果有朋友知道,还请指教一下。
### 根据变量长度是否确定
由于逃逸分析是在编译期就运行的,而不是在运行时运行的。因此避免有一些不定长的变量可能会很大,而在栈上分配内存失败,Go 会选择把这些变量统一在堆上申请内存,这是一种可以理解的保险的做法。
```
go
func
foo
()
{
length
:=
10
arr
:=
make
([]
int
,
0
,
length
)
// 由于容量是变量,因此不确定,因此在堆上申请
}
func
bar
()
{
arr
:=
make
([]
int
,
0
,
10
)
// 由于容量是常量,因此是确定的,因此在栈上申请
}
```
## 5. 参考文章
-
[
Go 语言内存管理三部曲(一)内存分配原理
](
https://xie.infoq.cn/article/ee1d2416d884b229dfe57bbcc
)
-
[
图解Go语言内存分配(面试重点,讲的很详细)
](
https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md
)
-
[
详解Go逃逸分析
](
https://zhuanlan.zhihu.com/p/343562181
)
\ No newline at end of file
source/c08/c08_03.rst
0 → 100644
浏览文件 @
290814a6
8.3 Go 语言中的内存分配规律及逃逸分析
=====================================
1. 分配内存三大组件
-------------------
Go 分配内存的过程,主要由三大组件所管理,级别从上到下分别是:
mheap
~~~~~
Go
在程序启动时,首先会向操作系统申请一大块内存,并交由\ ``mheap``\ 结构全局管理。
具体怎么管理呢? mheap
会将这一大块内存,切分成不同规格的小内存块,我们称之为
mspan,根据规格大小不同,mspan 大概有
70类左右,划分得可谓是非常的精细,足以满足各种对象内存的分配。
那么这些 mspan 大大小小的规格,杂乱在一起,肯定很难管理对吧?
因此就有了 mcentral 这下一级组件
mcentral
~~~~~~~~
启动一个 Go 程序,会初始化很多的 mcentral ,每个 mcentral
只负责管理一种特定规格的 mspan。
相当于 mcentral 实现了在 mheap 的基础上对 mspan 的精细化管理。
但是 mcentral 在 Go 程序中是全局可见的,因此如果每次协程来 mcentral
申请内存的时候,都需要加锁。
可以预想,如果每个协程都来 mcentral
申请内存,那频繁的加锁释放锁开销是非常大的。
因此需要有一个 mcentral 的二级代理来缓冲这种压力
mcache
~~~~~~
在一个 Go
程序里,每个线程\ ``M``\ 会绑定给一个处理器\ ``P``\ ,在单一粒度的时间里只能做多处理运行一个\ ``goroutine``\ ,每个\ ``P``\ 都会绑定一个叫
``mcache`` 的本地缓存。
当需要进行内存分配时,当前运行的\ ``goroutine``\ 会从\ ``mcache``\ 中查找可用的\ ``mspan``\ 。从本地\ ``mcache``\ 里分配内存时不需要加锁,这种分配策略效率更高。
mspan 供应链
~~~~~~~~~~~~
mcache 的 mspan 数量并不总是充足的,当供不应求的时候,mcache 会从
mcentral 再次申请更多的 mspan,同样的,如果 mcentral 的 mspan
数量也不够的话,mcentral 也会向它的上级 mheap 申请
mspan。再极端一点,如果 mheap 里的 mspan
也无法满足程序的内存申请,那该怎么办?
那就没办法啦,mheap 只能厚着脸皮跟操作系统这个老大哥申请了。
以上的供应流程,只适用于内存块小于 64KB 的场景,原因在于Go
没法使用工作线程的本地缓存\ ``mcache``\ 和全局中心缓存 ``mcentral``
上管理超过 64KB 的内存分配,所以对于那些超过 64KB
的内存申请,会直接从堆上(\ ``mheap``)上分配对应的数量的内存页(每页大小是
8KB)给程序。
2. 什么是堆内存和栈内存?
-------------------------
根据内存管理(分配和回收)方式的不同,可以将内存分为 **堆内存** 和
**栈内存**\ 。
那么他们有什么区别呢?
**堆内存**\ :由内存分配器和垃圾收集器负责回收
**栈内存**\ :由编译器自动进行分配和释放
一个程序运行过程中,也许会有多个栈内存,但肯定只会有一个堆内存。
每个栈内存都是由线程或者协程独立占有,因此从栈中分配内存不需要加锁,并且栈内存在函数结束后会自动回收,性能相对堆内存好要高。
而堆内存呢?由于多个线程或者协程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁,避免造成冲突,并且堆内存在函数结束后,需要
GC (垃圾回收)的介入参与,如果有大量的 GC
操作,将会吏程序性能下降得历害。
3. 逃逸分析的必要性
-------------------
由此可以看出,为了提高程序的性能,应当尽量减少内存在堆上分配,这样就能减少
GC 的压力。
在判断一个变量是在堆上分配内存还是在栈上分配内存,虽然已经有前人已经总结了一些规律,但依靠程序员能够在编码的时候时刻去注意这个问题,对程序员的要求相当之高。
好在 Go
的编译器,也开放了逃逸分析的功能,使用逃逸分析,可以直接检测出你程序员所有分配在堆上的变量(这种现象,即是逃逸)。
方法是执行如下命令
.. code:: shell
go build -gcflags '-m -l' demo.go
# 或者再加个 -m 查看更详细信息
go build -gcflags '-m -m -l' demo.go
4. 内存分配位置的规律
---------------------
如果逃逸分析工具,其实人工也可以判断到底有哪些变量是分配在堆上的。
那么这些规律是什么呢?
经过总结,主要有如下四种情况
1. 根据变量的使用范围
2. 根据变量类型是否确定
3. 根据变量的占用大小
4. 根据变量长度是否确定
接下来我们一个一个分析验证
根据变量的使用范围
~~~~~~~~~~~~~~~~~~
当你进行编译的时候,编译器会做逃逸分析(escape
analysis),当发现一个变量的使用范围仅在函数中,那么可以在栈上为它分配内存。
比如下边这个例子
.. code:: go
func foo() int {
v := 1024
return v
}
func main() {
m := foo()
fmt.Println(m)
}
我们可以通过 ``go build -gcflags '-m -l' demo.go``
来查看逃逸分析的结果,其中 ``-m`` 是打印逃逸分析的信息,\ ``-l``
则是禁止内联优化。
从分析的结果我们并没有看到任何关于 v
变量的逃逸说明,说明其并没有逃逸,它是分配在栈上的。
.. code:: go
$ go build -gcflags '-m -l' demo.go
# command-line-arguments
./demo.go:12:13: ... argument does not escape
./demo.go:12:13: m escapes to heap
而如果该变量还需要在函数范围之外使用,如果还在栈上分配,那么当函数返回的时候,该变量指向的内存空间就会被回收,程序势必会报错,因此对于这种变量只能在堆上分配。
比如下边这个例子,\ **返回的是指针**
.. code:: go
func foo() *int {
v := 1024
return &v
}
func main() {
m := foo()
fmt.Println(*m) // 1024
}
从逃逸分析的结果中可以看到 ``moved to heap: v`` ,v
变量是从堆上分配的内存,和上面的场景有着明显的区别。
.. code:: go
$ go build -gcflags '-m -l' demo.go
# command-line-arguments
./demo.go:6:2: moved to heap: v
./demo.go:12:13: ... argument does not escape
./demo.go:12:14: *m escapes to heap
除了返回指针之外,还有其他的几种情况也可归为一类:
**第一种情况:返回任意引用型的变量:Slice 和 Map**
.. code:: go
func foo() []int {
a := []int{1,2,3}
return a
}
func main() {
b := foo()
fmt.Println(b)
}
逃逸分析结果
.. code:: go
$ go build -gcflags '-m -l' demo.go
# command-line-arguments
./demo.go:6:12: []int literal escapes to heap
./demo.go:12:13: ... argument does not escape
./demo.go:12:13: b escapes to heap
**第二种情况:在闭包函数中使用外部变量**
.. code:: go
func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
in := Increase()
fmt.Println(in()) // 1
fmt.Println(in()) // 2
}
逃逸分析结果
.. code:: go
$ go build -gcflags '-m -l' demo.go
# command-line-arguments
./demo.go:6:2: moved to heap: n
./demo.go:7:9: func literal escapes to heap
./demo.go:15:13: ... argument does not escape
./demo.go:15:16: in() escapes to heap
根据变量类型是否确定
~~~~~~~~~~~~~~~~~~~~
在上边例子中,也许你发现了,所有编译输出的最后一行中都是
``m escapes to heap`` 。
奇怪了,为什么 m 会逃逸到堆上?
其实就是因为我们调用了 ``fmt.Println()`` 函数,它的定义如下
.. code:: go
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
可见其接收的参数类型是 ``interface{}``
,对于这种编译期不能确定其参数的具体类型,编译器会将其分配于堆上。
根据变量的占用大小
~~~~~~~~~~~~~~~~~~
最开始的时候,就介绍到,以 64KB 为分界线,我们将内存块分为 小内存块 和
大内存块。
小内存块走常规的 mspan 供应链申请,而大内存块则需要直接向
mheap,在堆区申请。
以下的例子来说明
.. code:: go
func foo() {
nums1 := make([]int, 8191) // < 64KB
for i := 0; i < 8191; i++ {
nums1[i] = i
}
}
func bar() {
nums2 := make([]int, 8192) // = 64KB
for i := 0; i < 8192; i++ {
nums2[i] = i
}
}
给 ``-gcflags`` 多加个 ``-m`` 可以看到更详细的逃逸分析的结果
.. code:: go
$ go build -gcflags '-m -l' demo.go
# command-line-arguments
./demo.go:5:15: make([]int, 8191) does not escape
./demo.go:12:15: make([]int, 8192) escapes to heap
那为什么是 64 KB 呢?
我只能说是试出来的
(8191刚好不逃逸,8192刚好逃逸),网上有很多文章千篇一律的说和
``ulimit -a`` 中的 ``stack size``
有关,但经过了解这个值表示的是系统栈的最大限制是 8192 KB,刚好是 8M。
.. code:: shell
$ ulimit -a
-t: cpu time (seconds) unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8192
我个人实在无法理解这个 8192 (8M) 和 64 KB
是如何对应上的,如果有朋友知道,还请指教一下。
根据变量长度是否确定
~~~~~~~~~~~~~~~~~~~~
由于逃逸分析是在编译期就运行的,而不是在运行时运行的。因此避免有一些不定长的变量可能会很大,而在栈上分配内存失败,Go
会选择把这些变量统一在堆上申请内存,这是一种可以理解的保险的做法。
.. code:: go
func foo() {
length := 10
arr := make([]int, 0 ,length) // 由于容量是变量,因此不确定,因此在堆上申请
}
func bar() {
arr := make([]int, 0 ,10) // 由于容量是常量,因此是确定的,因此在栈上申请
}
5. 参考文章
-----------
- `Go
语言内存管理三部曲(一)内存分配原理 <https://xie.infoq.cn/article/ee1d2416d884b229dfe57bbcc>`__
- `图解Go语言内存分配(面试重点,讲的很详细) <https://github.com/LeoYang90/Golang-Internal-Notes/blob/master/Go%20%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md>`__
- `详解Go逃逸分析 <https://zhuanlan.zhihu.com/p/343562181>`__
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录