整个包都只有一行有效代码,或许是一件值得思考的事情
闲逛GitHub的时候发现 Brad Fitzpatrick的iter包。仔细看了2遍。代码里确实只有一行有效代码
func N(n int) []struct{} {
return make([]struct{}, n)
}
刚开始也是一扫而过,然后看了看注释
It does not cause any allocations.
既然有这么多star还有几乎没提issue,我首先假定了他的注释是对的。立马想到空结构体 struct{} 是不占据空间的,典型的在写代码的时候,会经常这么写来判断某些值是否在之前出现过
m := make(map[string]struct{}, 0)
以及 空结构体的切片只占用切片头的空间。
但是关于切片的印象是占据24个字节,在64位机器上
var a []int
fmt.Println(unsafe.Sizeof(a))
// 这里会打印出来24
所以是否作者写的是错的,为什么说 函数 N 不会引发分配呢?
为了解决这个疑惑,需要先弄清楚两个问题:
图片来自 这里 图 6-1
Go 变量主要分为两种:
所以综上,对于在函数中定义的 Go 局部变量:要么被分配在堆上,要么被分配在栈上。
按照官方 FAQ How do I know whether a variable is allocated on the heap or the stack? 的解释:
package main
import "github.com/bradfitz/iter"
func main() {
for range iter.N(4) {}
}
go run -gcflags='-m -m' main.go
# command-line-arguments
./main.go:5:6: can inline main with cost 7 as: func() { for loop }
./main.go:6:18: inlining call to iter.N
./main.go:6:18: make([]struct {}, iter.n) escapes to heap:
./main.go:6:18: flow: {heap} = &{storage for make([]struct {}, iter.n)}:
./main.go:6:18: from make([]struct {}, iter.n) (non-constant size) at ./main.go:6:18
./main.go:6:18: make([]struct {}, iter.n) escapes to heap
按照前面的分析,从 “make([]struct {}, iter.n) escapes to heap” 的信息,推断:make([]struct {}, iter.n) 会被分配在堆上。
到这里,最初的疑惑似乎已经有了答案:make([]struct {}, iter.n) 一定会引发堆分配,那是 Brad Fitzpatrick 的注释写错了吗?
除了逃逸分析,Go 还提供了一种叫内存分配器追踪(Memory Allocator Trace)的方法,用于细粒度地分析由程序引发的所有堆分配(和释放)操作:
GODEBUG=allocfreetrace=1 go run main.go 2>&1 | grep -C 10
因为进行内存分配器追踪时,很多由 runtime 引发的分配信息也会被打印出来,所以用 grep 进行过滤,只显示由用户代码(user code)引发的分配信息。然而这里的输出结果为空,表明 make([]struct {}, iter.n) 没有引发任何堆分配。
内存分配器追踪的结论与逃逸分析的结论截然相反!那到底哪个结论是对的呢?
黔驴技穷之际,Go’s Memory Allocator - Overview 这篇文章给了提示:
So, we know that i is going to be allocated on the heap. But how does the runtime set that up? With the compiler’s help! We can get an idea from reading the generated assembly.
go tool compile -N -l -S main.go
0x0014 00020 (escape/p10/main.go:8) MOVQ AX, main.n+88(SP)
0x0019 00025 (escape/p10/main.go:8) MOVQ $0, main.~r0+24(SP)
0x0022 00034 (escape/p10/main.go:8) MOVUPS X15, main.~r0+32(SP)
0x0028 00040 (escape/p10/main.go:9) MOVQ main.n+88(SP), CX
0x002d 00045 (escape/p10/main.go:9) MOVQ main.n+88(SP), BX
0x0032 00050 (escape/p10/main.go:9) LEAQ type:struct {}(SB), AX
0x0039 00057 (escape/p10/main.go:9) PCDATA $1, $0
0x0039 00057 (escape/p10/main.go:9) CALL runtime.makeslice(SB)
可以看到,其中有一处对 runtime.makeslice(SB) 的调用,显然是由 make([]struct{}, n) 引发的。
查看 runtime.makeslice 的源码:
func makeslice(et *_type, len, cap int) slice {
...
p := mallocgc(et.size*uintptr(cap), et, true)
return slice{p, len, cap}
}
其中,mallocgc 的源码如下:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
if debug.allocfreetrace != 0 {
tracealloc(x, size, typ)
}
...
}
结合上述几段源码,可以看出:
经过一系列的探索和分析,至此,可以得出以下结论:
最后,来解答文章标题提出的疑问 —— 如何确定一个 Go 变量会被分配在哪里?对此:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。