为什么在Go语言中,栈溢出取决于如何访问数组?

huangapple go评论73阅读模式
英文:

Why is the stack overflow depending on how to access the array in Go?

问题

考虑以下Go程序:

package main

func main() {
    var buffer [100000000]float64
    var i int
    for i = range buffer {
        buffer[i] = float64(i)
    }
}

使用"go run test1.go"命令可以正常运行(除非你的RAM太少)。

现在,我对该程序进行了微小的扩展:

package main

func main() {
    var buffer [100000000]float64
    var i int
    var value float64
    for i, value = range buffer {
        value = value
        buffer[i] = float64(i)
    }
}

使用"go run test2.go"命令会产生以下错误:

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

runtime stack:
runtime.throw(0x473350, 0xe)
        /usr/local/go/src/runtime/panic.go:527 +0x90
runtime.newstack()
        /usr/local/go/src/runtime/stack1.go:794 +0xb17
runtime.morestack()
        /usr/local/go/src/runtime/asm_amd64.s:330 +0x7f

goroutine 1 [stack growth]:
main.main()
        /home/bronger/src/go-test/test3.go:3 fp=0xc860075e50 sp=0xc860075e48
runtime.main()
        /usr/local/go/src/runtime/proc.go:111 +0x2b0 fp=0xc860075ea0 sp=0xc860075e50
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1696 +0x1 fp=0xc860075ea8 sp=0xc860075ea0
exit status 2

我觉得在test1.go中使用了堆,而在test2.go中使用了栈。为什么会这样呢?

英文:

Consider the following Go program:

package main

func main() {
    var buffer [100000000]float64
    var i int
    for i = range buffer {
	    buffer[i] = float64(i)
    }
}

With "go run test1.go", it works. (Unless you have too little RAM.)

Now, I expand this program trivially:

package main

func main() {
    var buffer [100000000]float64
    var i int
    var value float64
    for i, value = range buffer {
	    value = value
	    buffer[i] = float64(i)
    }
}

"go run test2.go" yields:

<!-- language: lang-none -->

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

runtime stack:
runtime.throw(0x473350, 0xe)
        /usr/local/go/src/runtime/panic.go:527 +0x90
runtime.newstack()
        /usr/local/go/src/runtime/stack1.go:794 +0xb17
runtime.morestack()
        /usr/local/go/src/runtime/asm_amd64.s:330 +0x7f

goroutine 1 [stack growth]:
main.main()
        /home/bronger/src/go-test/test3.go:3 fp=0xc860075e50 sp=0xc860075e48
runtime.main()
        /usr/local/go/src/runtime/proc.go:111 +0x2b0 fp=0xc860075ea0 sp=0xc860075e50
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1696 +0x1 fp=0xc860075ea8 sp=0xc860075ea0
exit status 2

It seems to me that in test1.go, the heap was used, whereas in test2.go, the stack was used. Why?

答案1

得分: 9

根据Go规范

在循环开始之前,range表达式会被评估一次,但有一个例外:如果range表达式是一个数组或指向数组的指针,并且最多只有一个迭代变量存在,则只评估range表达式的长度。

因此,在第一个程序中,只评估了buffer的长度并将其放置在堆栈上。

在第二个程序中,在迭代之前,整个buffer都被放置在堆栈上。由于编译期间已知buffer的大小,Go编译器会在函数开头放置指令来预分配堆栈空间。这就是为什么panic跟踪指向函数开头的原因。

在这两种情况下,buffer都在堆上分配。可以通过以下方式确认:

$ go build -gcflags=-m
./main.go:4: moved to heap: buffer

请注意,如果将buffer更改为全局变量,程序的行为将类似。

但是,如果将buffer更改为切片(buffer := make([]float64, 100000000)),则两种情况下程序都会成功。这是因为切片只是指向实际数组的指针,并且它在堆栈上只占用几个字节,与支持数组的大小无关。因此,修复第二个程序的最简单方法是使其迭代切片而不是数组:

....
for i, value = range buffer[:] {
    ....
}

令人惊讶的是,如果尝试创建一个更大的数组[1000000000]float64,编译器会报错(堆栈帧太大(>2GB))。我认为它应该在编译第二个程序时也报错,而不是让它发生panic。你可能想要将此问题报告给http://github.com/golang/go/issues

英文:

According to the Go specification:

> The range expression is evaluated once before beginning the loop, with one exception: if the range expression is an array or a pointer to an array and at most one iteration variable is present, only the range expression's length is evaluated

So in the first program only the length of buffer is evaluated and placed on the stack.

In the second program the whole buffer is placed on the stack before iterating over it. Because the size of the buffer is known during compilation the Go compiler places instruction at the beginning of the function to pre-allocate stack space. That is why panic trace points to the beginning of the function.

In both cases buffer is allocated on the heap. This can be confirmed by

$ go build -gcflags=-m
./main.go:4: moved to heap: buffer

Note that the program will behave similarly if you make buffer a global variable.

However if you change buffer to be a slice (buffer := make([]float64, 100000000)) the program will succeed in both cases. This happens because a slice is just a pointer to the actual array and it takes only a few bytes on the stack independently of the backing array size. So the simplest way to fix your second program is to make it iterate over slice instead of array:

....
for i, value = range buffer[:] {
    ....
}

Surprisingly if you try to create a bigger array [1000000000]float64 then the compiler will complain (stack frame too large (>2GB)). I think it should complain when compiling your second program as well instead of letting it panic. You might want to report this issue to http://github.com/golang/go/issues

huangapple
  • 本文由 发表于 2016年1月19日 09:43:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/34867195.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定