# Go 协程堆栈设计进化之旅

- 后端早读课翻译计划 第四篇-

- 翻译自： a-journey-with-go

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ 文章基于 Go 1.12.

Go 提供了一个轻量且智能的协程管理机制。轻量是因为协程堆栈初始化只有 2Kb，智能是因为协程堆栈可以根据我们的需要自动增加 / 减少。

``````// The minimum size of stack used by Go code
_StackMin = 2048
``````

• Go 1.2: 协程堆栈从 4Kb 增长到 8Kb.

• Go 1.4: 协程堆栈从 8Kb 减少到 2Kb.

# 动态堆栈大小

``````func main() {
a := 1
b := 2

r := max(a, b)
println(`max: `+strconv.Itoa(r))
}

func max(a int, b int) int {
if a >= b {
return a
}

return b
}
``````

``````

"".main STEXT size=186 args=0x0 locals=0x70
0x0000 00000 (/go/src/main.go:5)    TEXT   "".main(SB),
ABIInternal, \$112-0
[...]
0x00b0 00176 (/go/src/main.go:5) CALL
runtime.morestack_noctxt(SB)
[...]
0x0000 00000 (/go/src/main.go:13)    TEXT   "".max(SB),
NOSPLIT|ABIInternal, \$0-24

- CALL runtime.morestack_noctxt: 这个方法会在需要的时候增加堆栈大小。

-NOSPLIT: 这条指令的意味着堆栈不需要溢出检测，他与指令  //go:nosplit .比较相似。
``````

``````
func newstack() {
[...]
// Allocate a bigger segment and move the stack.
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2
if newsize > maxstacksize {
print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
throw("stack overflow")
}

// The goroutine must be executing in order to call newstack,
// so it must be Grunning (or Gscanrunning).
casgstatus(gp, _Grunning, _Gcopystack)

// The concurrent GC will not scan the stack while we are doing the copy since
// the gp is in a Gcopystack status.
copystack(gp, newsize, true)
if stackDebug >= 1 {
print("stack grow done\n")
}
casgstatus(gp, _Gcopystack, _Grunning)
}
``````

``````
type stack struct {
lo uintptr
hi uintptr
}
``````

``````
// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
``````

``````
func main() {
var x [10]int
a(x)
}

//go:noinline
func a(x [10]int) {
println(`func a`)
var y [100]int
b(y)
}

//go:noinline
func b(x [100]int) {
println(`func b`)
var y [1000]int
c(y)
}

//go:noinline
func c(x [1000]int) {
println(`func c`)
}
``````

`//go:noinline` 指令是为了避免编译时把所有的方法都放到一行。如果都放到一行的话，我们将看不到每个方法开始时候的堆栈动态增长。

``````runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]
stack grow done
func a
runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]
stack grow done
runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000]
stack grow done
runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000]
stack grow done
func b
runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000]
func c
``````

``````runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]
0xc00002e800 - 0xc00002e000 = 2048
runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]
0xc000077000 - 0xc000076000 = 4096
runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000]
0xc000040000 - 0xc00003e000 = 8192
runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000]
0xc000082000 - 0xc00007e000 = 16384
runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000]
0xc00008a000 - 0xc000082000 = 32768
``````

# 堆栈分配管理

``````runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]
copystack gp=0xc000000300 [0xc00002e000 0xc00002e6e0 0xc00002e800] -> [0xc000076000 0xc000076ee0 0xc000077000]/4096
stackfree 0xc00002e000 2048
stack grow done
runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]
copystack gp=0xc000000300 [0xc000076000 0xc000076890 0xc000077000] -> [0xc00003e000 0xc00003f890 0xc000040000]/8192
stackfree 0xc000076000 4096
stack grow done
``````

Golang stack growth with contiguous stack

`copystack` 指令复制了整个堆栈，并把所有的地址都移向新的堆栈。我们可以通过一段简短的代码来很容易的发现这个现象：

``````func main() {
var x [10]int
println(&x)
a(x)
println(&x)
}
``````

``````0xc00002e738
[...]
0xc000089f38
``````

``````func main() {
var x [10]int
println(&x)
a(x)
runtime.GC()
println(&x)
}
``````

Debug 程序会展示出堆栈缩减的日志：

``````func c
shrinking stack 32768->16384
copystack gp=0xc000000300 [0xc000082000 0xc000089e60 0xc00008a000] -> [0xc00007e000 0xc000081e60 0xc000082000]/16384
``````

``````oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize / 2
``````

# 连续堆栈 VS 分段堆栈

``````func a
runtime: newstack framesize=0x3e90 argsize=0x320 sp=0x7f8875953848 stack=[0x7f8875952000, 0x7f8875953fa0]
-> new stack [0xc21001d000, 0xc210021950]
func b
func c
runtime: oldstack gobuf={pc:0x400cff sp:0x7f8875953858 lr:0x0} cret=0x1 argsize=0x320
``````

`0x7f8875953fa0 - 0x7f8875952000` 并不到 8Kb，应该是笔误，应该是 8096 字节）

``````// allocate new segment.
framesize += argsize;
framesize += StackExtra;   // room for more functions, Stktop.
if(framesize < StackMin)
framesize = StackMin;
framesize += StackSystem;
``````

Golang stack growth with segmented stack

# 总结

Go 的堆栈管理是非常高效的，而且容易理解。Golang 不是唯一一个没有选择分段堆栈的语言， Rust 语言因为同样的原因而没有选择这个方案。

# 扩展阅读：

contiguous stack

contiguous stack in Go 1.3

StackExtra

StackMin

StackSystem

frames in the stack

0 回复

• 请尽量让自己的回复能够对别人有帮助
• 支持 Markdown 格式, **粗体**、~~删除线~~、``单行代码``
• 支持 @ 本站用户；支持表情（输入 : 提示），见 Emoji cheat sheet
• 图片支持拖拽、截图粘贴等方式上传

- 后端早读课翻译计划 第四篇-

- 翻译自： a-journey-with-go

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ 文章基于 Go 1.12.

Go 提供了一个轻量且智能的协程管理机制。轻量是因为协程堆栈初始化只有 2Kb，智能是因为协程堆栈可以根据我们的需要自动增加 / 减少。

``````// The minimum size of stack used by Go code
_StackMin = 2048
``````

• Go 1.2: 协程堆栈从 4Kb 增长到 8Kb.

• Go 1.4: 协程堆栈从 8Kb 减少到 2Kb.

# 动态堆栈大小

``````func main() {
a := 1
b := 2

r := max(a, b)
println(`max: `+strconv.Itoa(r))
}

func max(a int, b int) int {
if a >= b {
return a
}

return b
}
``````

``````

"".main STEXT size=186 args=0x0 locals=0x70
0x0000 00000 (/go/src/main.go:5)    TEXT   "".main(SB),
ABIInternal, \$112-0
[...]
0x00b0 00176 (/go/src/main.go:5) CALL
runtime.morestack_noctxt(SB)
[...]
0x0000 00000 (/go/src/main.go:13)    TEXT   "".max(SB),
NOSPLIT|ABIInternal, \$0-24

- CALL runtime.morestack_noctxt: 这个方法会在需要的时候增加堆栈大小。

-NOSPLIT: 这条指令的意味着堆栈不需要溢出检测，他与指令  //go:nosplit .比较相似。
``````

``````
func newstack() {
[...]
// Allocate a bigger segment and move the stack.
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2
if newsize > maxstacksize {
print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
throw("stack overflow")
}

// The goroutine must be executing in order to call newstack,
// so it must be Grunning (or Gscanrunning).
casgstatus(gp, _Grunning, _Gcopystack)

// The concurrent GC will not scan the stack while we are doing the copy since
// the gp is in a Gcopystack status.
copystack(gp, newsize, true)
if stackDebug >= 1 {
print("stack grow done\n")
}
casgstatus(gp, _Gcopystack, _Grunning)
}
``````

``````
type stack struct {
lo uintptr
hi uintptr
}
``````

``````
// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
``````

``````
func main() {
var x [10]int
a(x)
}

//go:noinline
func a(x [10]int) {
println(`func a`)
var y [100]int
b(y)
}

//go:noinline
func b(x [100]int) {
println(`func b`)
var y [1000]int
c(y)
}

//go:noinline
func c(x [1000]int) {
println(`func c`)
}
``````

`//go:noinline` 指令是为了避免编译时把所有的方法都放到一行。如果都放到一行的话，我们将看不到每个方法开始时候的堆栈动态增长。

``````runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]
stack grow done
func a
runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]
stack grow done
runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000]
stack grow done
runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000]
stack grow done
func b
runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000]
func c
``````

``````runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]
0xc00002e800 - 0xc00002e000 = 2048
runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]
0xc000077000 - 0xc000076000 = 4096
runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000]
0xc000040000 - 0xc00003e000 = 8192
runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000]
0xc000082000 - 0xc00007e000 = 16384
runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000]
0xc00008a000 - 0xc000082000 = 32768
``````

# 堆栈分配管理

``````runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]
copystack gp=0xc000000300 [0xc00002e000 0xc00002e6e0 0xc00002e800] -> [0xc000076000 0xc000076ee0 0xc000077000]/4096
stackfree 0xc00002e000 2048
stack grow done
runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]
copystack gp=0xc000000300 [0xc000076000 0xc000076890 0xc000077000] -> [0xc00003e000 0xc00003f890 0xc000040000]/8192
stackfree 0xc000076000 4096
stack grow done
``````

Golang stack growth with contiguous stack

`copystack` 指令复制了整个堆栈，并把所有的地址都移向新的堆栈。我们可以通过一段简短的代码来很容易的发现这个现象：

``````func main() {
var x [10]int
println(&x)
a(x)
println(&x)
}
``````

``````0xc00002e738
[...]
0xc000089f38
``````

``````func main() {
var x [10]int
println(&x)
a(x)
runtime.GC()
println(&x)
}
``````

Debug 程序会展示出堆栈缩减的日志：

``````func c
shrinking stack 32768->16384
copystack gp=0xc000000300 [0xc000082000 0xc000089e60 0xc00008a000] -> [0xc00007e000 0xc000081e60 0xc000082000]/16384
``````

``````oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize / 2
``````

# 连续堆栈 VS 分段堆栈

``````func a
runtime: newstack framesize=0x3e90 argsize=0x320 sp=0x7f8875953848 stack=[0x7f8875952000, 0x7f8875953fa0]
-> new stack [0xc21001d000, 0xc210021950]
func b
func c
runtime: oldstack gobuf={pc:0x400cff sp:0x7f8875953858 lr:0x0} cret=0x1 argsize=0x320
``````

`0x7f8875953fa0 - 0x7f8875952000` 并不到 8Kb，应该是笔误，应该是 8096 字节）

``````// allocate new segment.
framesize += argsize;
framesize += StackExtra;   // room for more functions, Stktop.
if(framesize < StackMin)
framesize = StackMin;
framesize += StackSystem;
``````

Golang stack growth with segmented stack

# 总结

Go 的堆栈管理是非常高效的，而且容易理解。Golang 不是唯一一个没有选择分段堆栈的语言， Rust 语言因为同样的原因而没有选择这个方案。

# 扩展阅读：

contiguous stack

contiguous stack in Go 1.3

StackExtra

StackMin

StackSystem