Go:空花括号对数组初始化内存分配的影响

5

我正在尝试使用不同的方法在golang中初始化/声明数组。我得到了不同的行为/结果。

go版本go1.3 darwin/amd64

版本1:

func main() {
    a := [100000000]int64{}
    var i int64
    for i = 0; i < 100000000; i++ {
        a[i] = i
    }
}

生成一个763MB的二进制文件。当我运行它时,几秒钟后就会崩溃并显示以下信息:

运行时错误:goroutine堆栈超出1000000000字节限制

致命错误:堆栈溢出

版本2:

func main() {
    var a [100000000]int64
    var i int64
    for i = 0; i < 100000000; i++ {
        a[i] = i
    }
}

产生一个456KB的二进制文件,运行时间不到一秒钟。

问题:

有人可以帮我理解为什么存在这些差异(和其他可能被忽略的差异)吗?谢谢!

编辑:

运行时间:

我构建了两个不同的代码片段并运行了编译版本,所以没有添加编译时间。虽然第一次运行version1非常慢。以下是输出结果。

go build version1.go
go build version2.go

以下是执行输出结果

版本 1

第一次运行

time ./version1
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

runtime stack:
runtime.throw(0x2fb42a8e)
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/panic.c:520 +0x69
runtime.newstack()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/stack.c:770 +0x486
runtime.morestack()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/asm_amd64.s:228 +0x61

goroutine 16 [stack growth]:
main.main()
    /Users/ec/repo/offers/lol/version1.go:3 fp=0x2b7b85f50 sp=0x2b7b85f48
runtime.main()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/proc.c:247 +0x11a fp=0x2b7b85fa8 sp=0x2b7b85f50
runtime.goexit()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/proc.c:1445 fp=0x2b7b85fb0 sp=0x2b7b85fa8
created by _rt0_go
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/asm_amd64.s:97 +0x120

goroutine 17 [runnable]:
runtime.MHeap_Scavenger()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/mheap.c:507
runtime.goexit()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/proc.c:1445
./version1  0.00s user 0.10s system 1% cpu 7.799 total

第二次运行

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

runtime stack:
runtime.throw(0x2fb42a8e)
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/panic.c:520 +0x69
runtime.newstack()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/stack.c:770 +0x486
runtime.morestack()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/asm_amd64.s:228 +0x61

goroutine 16 [stack growth]:
main.main()
    /Users/ec/repo/offers/lol/version1.go:3 fp=0x2b7b85f50 sp=0x2b7b85f48
runtime.main()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/proc.c:247 +0x11a fp=0x2b7b85fa8 sp=0x2b7b85f50
runtime.goexit()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/proc.c:1445 fp=0x2b7b85fb0 sp=0x2b7b85fa8
created by _rt0_go
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/asm_amd64.s:97 +0x120

goroutine 17 [runnable]:
runtime.MHeap_Scavenger()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/mheap.c:507
runtime.goexit()
    /usr/local/Cellar/go/1.3/libexec/src/pkg/runtime/proc.c:1445
./version1  0.00s user 0.10s system 98% cpu 0.102 total

版本 2

首次运行

time ./version2
./version2  0.16s user 0.26s system 99% cpu 0.429 total

第二次运行

time ./version2
./version2  0.17s user 0.25s system 97% cpu 0.421 total

请注意,这两个程序都会非常快速地执行。我假设您正在使用go run进行测试,并且时间差在于构建大型二进制文件。 - JimB
两者都可以快速执行,但第一次运行版本1时不行。我在之前的步骤中构建了它们。我在原始帖子中添加了更多信息。 - isaacbernat
1
@prok05 第一次运行v1需要一些时间是因为操作系统正在尝试将700MB的文件缓存到内存中,第二次,操作系统/文件系统已经将其缓存,因此启动更快。 - OneOfOne
2个回答

2
在版本1中,您声明了一个[100000000]int64{}的文本数组,编译器立即分配内存。
在版本2中,您只声明了a的类型为[100000000]int64
当您只有变量声明时,在编译期间无法确定其内容。在版本2中,编译器知道a的类型为[100000000]int64,但是内存直到运行时才会分配。
当您使用文字标记时,确切的内存表示将写入二进制文件。它与声明string文字与string类型变量相同;字符串文字将被写入指定位置,而变量声明仅是占位符。
尽管当前的编译器(go 1.3)允许a逃逸到堆上,但文字数据应该存在于栈帧中。您可以在汇编输出中看到这一点(框架大小为800000016)。
TEXT    "".func1+0(SB),$800000016-0

如果您需要一个比堆栈容量更大的文字量,可以将其放置在全局变量中。以下代码可以正常执行:
var a = [100000000]int64{1}

func func1() {
    var i int64
    for i = 0; i < 100000000; i++ {
        a[i] = i
    }
}

我必须在此处初始化至少一个值a,因为似乎编译器可以省略这个字面量,如果它等于零值。


是的,但那并没有真正解释发生了什么,对吧(除了二进制文件的大小)? - Denys Séguret
什么意思,这只是一个占位符?数组已经分配并填充了。 - Denys Séguret
@dystroy: 我的意思是它不是预先分配的(显然,因为二进制文件<500KB),内存是在运行时分配的。这解释了二进制文件大小,但我不确定为什么版本1会出现堆栈溢出,因为编译器确实将a转义到堆上。 - JimB
2
但是在某种方式上初始化之前,它不需要保留那个内存。它已经被初始化了。如果声明一个变量而没有显式地初始化它,则会隐式地将其初始化为该类型的零值,对于数组来说,这意味着所有元素都被初始化为零值,这应该与使用空括号初始化它相同。从来没有“未初始化的变量”。 - newacct
1
当有花括号时,您可能会将其初始化为非零值(可以说a:= [2] int {1,2}),我怀疑这会促使Go将您要初始化的整个值存储在二进制文件中(通常有效;大多数文字不是巨大的)。当没有括号时,编译器“知道”它只需运行代码以填充内存,因此它不会在二进制文件中存储任何内容。 - twotwotwo
显示剩余5条评论

1

我不是一个真正的答案,但也许有人会发现这个有用。

在我的情况下,当我尝试对以下结构进行json.Marshal时,就会出现这种情况:

type Element struct {
    Parent *Element
    Child []*Element
}

父级指向包含当前元素的子级元素。
因此,我只需将“Parent”标记为json:“ - ”以在编组时忽略它。

那真的让我摆脱了引用循环,谢谢!正是我所需要的。 - SiennaD.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接