Go 1.19 average goroutine stack

1,218 views
Skip to first unread message

梁智堯Brian Liang

unread,
Aug 13, 2022, 10:00:58 AM8/13/22
to golang-nuts
Hi masters,

As far as I know, go 1.19 supports self-adaptive stack size when spawning a goroutine, which intends to decrease the calling times of morestack that aims to claim more frames from heap.

After each GC happens, Go runtime will calculate the average stack usage and the next time, goroutine would created with such a stack size.

My question is, how do we validate the whole process or check it if works well or not.
Is there any metric or stack size checking function, or should I use pprof to peek the alloc part ?

Kindly thanks for all

```
func main() {
go func() {
// spawn a goroutine started from a fixed stack size
}()

runtime.GC()

go func() {
// spawn a goroutine started from an avg stack size.
}()
}
```


Keith Randall

unread,
Aug 14, 2022, 11:44:39 AM8/14/22
to golang-nuts
The initial allocation size is exported, you can use the runtime/metrics package to look at it. Something like this:

package main

import (
    "fmt"
    "runtime/metrics"
)

func main() {
    s := []metrics.Sample{{Name: "/gc/stack/starting-size:bytes"}}
    metrics.Read(s)
    fmt.Printf("%d\n", s[0].Value.Uint64())
}

Mateusz Poliwczak

unread,
Aug 17, 2022, 6:52:42 PM8/17/22
to golang-nuts
For me the adaptive stack size does not work as expected.

[mateusz@arch avg]$ go version
go version devel go1.20-c8000a18d6 Mon Aug 15 17:07:57 2022 +0000 linux/amd6

I've set the stackDebug in runtine to 1.
stackdebug runtime output: https://pastebin.com/sLLfw62E
It doesn't seem to use the avg stack size (it still allocates the default 2KB, and resizes when necessary)

tapi...@gmail.com

unread,
Aug 17, 2022, 11:18:35 PM8/17/22
to golang-nuts
I'm a bit wondering about how the following case will be affected by the change:
1. Initially, there is one goroutine, which stack size is large at the time of a GC process.
2. After the GC process, a large quantity of goroutines start. They all need small stacks.
   But now the runtime will allocate a very large stack for each of them.

Then is much memory wasted? Will the stacks of the new goroutines shrink at the next GC process?

T L

unread,
Aug 18, 2022, 4:34:42 AM8/18/22
to Kurtis Rader, golang-nuts


On Thu, Aug 18, 2022 at 11:30 AM Kurtis Rader <kra...@skepticism.us> wrote:
On Wed, Aug 17, 2022 at 8:18 PM tapi...@gmail.com <tapi...@gmail.com> wrote:
I'm a bit wondering about how the following case will be affected by the change:
1. Initially, there is one goroutine, which stack size is large at the time of a GC process.
2. After the GC process, a large quantity of goroutines start. They all need small stacks.
   But now the runtime will allocate a very large stack for each of them.

Then is much memory wasted? Will the stacks of the new goroutines shrink at the next GC process?

I can't help but wonder why, if you care so much about this type of issue, you did not make any attempt to answer your own question? Why not assume the Go team is competent (thus handling your scenario in a reasonable, if not optimal, fashion) unless you have evidence to the contrary? Why didn't you take a few minutes to write a Go program to test your own hypothesis? Questions are great unless you make no attempt to answer your own question.


So you don't care so much about this type of issue? I wonder why you don't care?

When I investigate something, I ask questions in communities firstly, to save time.
For experts who understand the problem, it will spend them no much time to make an answer.

I never denied the competentness of Go team.

I don't think I'm able to write a Go program to test in a few minutes.
In fact, this will be my last attempt and is why I asked the question here.
If you are able to, could you write one? I will be very appreciate it.
 
 
On Saturday, August 13, 2022 at 10:00:58 PM UTC+8 lia...@garena.com wrote:
Hi masters,

As far as I know, go 1.19 supports self-adaptive stack size when spawning a goroutine, which intends to decrease the calling times of morestack that aims to claim more frames from heap.

After each GC happens, Go runtime will calculate the average stack usage and the next time, goroutine would created with such a stack size.

My question is, how do we validate the whole process or check it if works well or not.
Is there any metric or stack size checking function, or should I use pprof to peek the alloc part ?

Kindly thanks for all

```
func main() {
go func() {
// spawn a goroutine started from a fixed stack size
}()

runtime.GC()

go func() {
// spawn a goroutine started from an avg stack size.
}()
}
```


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/e922b755-8205-462b-91ed-b9391b6fa2b2n%40googlegroups.com.


--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Jan Mercl

unread,
Aug 18, 2022, 5:11:51 AM8/18/22
to T L, Kurtis Rader, golang-nuts
On Thu, Aug 18, 2022 at 10:34 AM T L <tapi...@gmail.com> wrote:

> When I investigate something, I ask questions in communities firstly, to save time.

To save your time at the expense of more time wasted by others. Such
an approach is rightfully frowned upon.

Doing your own research first, asking about things where you got stuck
next is fine.

T L

unread,
Aug 18, 2022, 5:43:28 AM8/18/22
to Jan Mercl, Kurtis Rader, golang-nuts
It really wastes your time (and others') to make such an unnecessary and impolite reply.

Keith Randall

unread,
Aug 18, 2022, 12:14:55 PM8/18/22
to golang-nuts
On Wednesday, August 17, 2022 at 8:18:35 PM UTC-7 tapi...@gmail.com wrote:
I'm a bit wondering about how the following case will be affected by the change:
1. Initially, there is one goroutine, which stack size is large at the time of a GC process.
2. After the GC process, a large quantity of goroutines start. They all need small stacks.
   But now the runtime will allocate a very large stack for each of them.

Then is much memory wasted?

Kind of. Your description is correct, the runtime will allocate larger stacks for each of the new goroutines. But it doesn't really waste memory, it just causes the program to reach the next GC earlier. At that GC, stacks will shrink, as will the start size for subsequent goroutines. So it won't really waste memory, but waste some CPU for a possibly extra GC.
It isn't perfect; for instance we only shrink stacks 2x at each GC, so if the starting size is way too large it might take a couple GCs to shrink sufficiently.
 
Will the stacks of the new goroutines shrink at the next GC process?

Yes. The larger stack sizes are only on goroutine start. The runtime doesn't forbid shrinking below the start size.

tapi...@gmail.com

unread,
Aug 20, 2022, 10:58:10 AM8/20/22
to golang-nuts
On Friday, August 19, 2022 at 12:14:55 AM UTC+8 k...@google.com wrote:
On Wednesday, August 17, 2022 at 8:18:35 PM UTC-7 tapi...@gmail.com wrote:
I'm a bit wondering about how the following case will be affected by the change:
1. Initially, there is one goroutine, which stack size is large at the time of a GC process.
2. After the GC process, a large quantity of goroutines start. They all need small stacks.
   But now the runtime will allocate a very large stack for each of them.

Then is much memory wasted?

Kind of. Your description is correct, the runtime will allocate larger stacks for each of the new goroutines. But it doesn't really waste memory, it just causes the program to reach the next GC earlier. At that GC, stacks will shrink, as will the start size for subsequent goroutines. So it won't really waste memory, but waste some CPU for a possibly extra GC.
It isn't perfect; for instance we only shrink stacks 2x at each GC, so if the starting size is way too large it might take a couple GCs to shrink sufficiently.

I wrote a program to make some tests: https://go.dev/play/p/oDYrAsZz_3i

It looks the new GC pacer doesn't count new stack memory as new allocated heap memory.
So large new stacks will not make the program to reach the next GC earlier.

Since Go 1.18, the stack sizes (the real sizes, not the 2^n sizes) and global pointers contribute in calculating target heap memory:
https://go.dev/doc/gc-guide#GOGC, so that large stack sizes will cause large GC intervals.

It seems that the outputs indicate there are some hidden goroutines which cause the calculated average stack sizes
much smaller than expected.

BTW, in the runtime docs, https://pkg.go.dev/runtime, "# MB stacks" and "# MB globals" are listed but not shown in the format line.

tapi...@gmail.com

unread,
Aug 20, 2022, 11:00:35 AM8/20/22
to golang-nuts
BTW, the outputs of the test program:

$ GODEBUG=gctrace=1  ./main
gc 1 @0.006s 4%: 0.035+1.5+0.006 ms clock, 0.14+0/1.5/2.0+0.024 ms cpu, 0->0->0 MB, 8 MB goal, 0 MB stacks, 8 MB globals, 4 P (forced)
2022/08/20 22:46:47 2048 1
gc 2 @2.123s 0%: 0.034+2.6+0.008 ms clock, 0.13+0/2.5/7.1+0.035 ms cpu, 0->0->0 MB, 8 MB goal, 64 MB stacks, 8 MB globals, 4 P (forced)
2022/08/20 22:46:49 8388608 2
GC forced
gc 3 @124.390s 0%: 0.045+5.1+0.010 ms clock, 0.18+0/5.1/0+0.041 ms cpu, 0->0->0 MB, 63 MB goal, 128 MB stacks, 8 MB globals, 4 P
GC forced
gc 4 @244.411s 0%: 0.072+5.0+0.009 ms clock, 0.28+0/5.0/0+0.038 ms cpu, 0->0->0 MB, 118 MB goal, 128 MB stacks, 8 MB globals, 4 P
gc 5 @307.129s 0%: 0.076+2.7+0.013 ms clock, 0.30+0/2.7/7.9+0.052 ms cpu, 0->0->0 MB, 118 MB goal, 128 MB stacks, 8 MB globals, 4 P (forced)
2022/08/20 22:51:56 16777216 3
gc 6 @313.134s 0%: 0.098+2.6+0.011 ms clock, 0.39+0/2.5/7.5+0.044 ms cpu, 0->0->0 MB, 118 MB goal, 128 MB stacks, 8 MB globals, 4 P (forced)
2022/08/20 22:52:02 16777216 3

tapi...@gmail.com

unread,
Aug 22, 2022, 10:31:39 AM8/22/22
to golang-nuts
On Saturday, August 20, 2022 at 10:58:10 PM UTC+8 tapi...@gmail.com wrote:
On Friday, August 19, 2022 at 12:14:55 AM UTC+8 k...@google.com wrote:
On Wednesday, August 17, 2022 at 8:18:35 PM UTC-7 tapi...@gmail.com wrote:
I'm a bit wondering about how the following case will be affected by the change:
1. Initially, there is one goroutine, which stack size is large at the time of a GC process.
2. After the GC process, a large quantity of goroutines start. They all need small stacks.
   But now the runtime will allocate a very large stack for each of them.

Then is much memory wasted?

Kind of. Your description is correct, the runtime will allocate larger stacks for each of the new goroutines. But it doesn't really waste memory, it just causes the program to reach the next GC earlier. At that GC, stacks will shrink, as will the start size for subsequent goroutines. So it won't really waste memory, but waste some CPU for a possibly extra GC.
It isn't perfect; for instance we only shrink stacks 2x at each GC, so if the starting size is way too large it might take a couple GCs to shrink sufficiently.

I wrote a program to make some tests: https://go.dev/play/p/oDYrAsZz_3i

It looks the new GC pacer doesn't count new stack memory as new allocated heap memory.
So large new stacks will not make the program to reach the next GC earlier.

Since Go 1.18, the stack sizes (the real sizes, not the 2^n sizes) and global pointers contribute in calculating target heap memory:
https://go.dev/doc/gc-guide#GOGC, so that large stack sizes will cause large GC intervals.

More precisely, Go 1.18 uses the 2^n stack sizes, whereas Go 1.19 uses the real stack sizes,
Reply all
Reply to author
Forward
0 new messages