Use new heap memory percentage strategy to control automatic GC frequency
Tags: go
Published: 2023-03-30 07:21:13

The value parts which contain pointers and are known clearly alive by Go runtime are called (scannable) root value parts (call roots below). Value parts allocated on stack and direct parts of global (package-level) variables are the main sources of roots. To simplify the implementation, the official Go runtime views all value parts allocated on the stack as roots, including value parts without pointers. Root memory blocks host roots. For the official standard Go runtime, before version 1.18, roots had no impact on the new heap memory percentage strategy; since version 1.18, they have. The new heap memory percentage strategy is configured through a GOGC value, which may be set via the GOGC environment variable or modified by calling the runtime/debug.SetGCPercent function. The default value of the GOGC value is 100. We may set the GOGC environment variable as off or call the runtime/debug.SetGCPercent function with a negative argument to disable the new heap memory percentage strategy. Note: doing these will also disable the two-minute auto GC strategy. To disable the new heap memory percentage strategy solely, we may set the GOGC value as math.MaxInt64. If the new heap memory percentage strategy is enabled (the GOGC value is non-negative), when the scan+mark phase of a GC cycle is just done, the official standard Go runtime (version 1.18+) will calculate the target heap size for the next garbage collection cycle, from the non-garbage heap memory total size (call live heap here) and the root memory block total size (called GC roots here).

according to the following formula:

`Target heap size = Live heap + (Live heap + GC roots) * GOGC / 100`

For versions before 1.18, the formula is:

`Target heap size = Live heap + (Live heap) * GOGC / 100`

If the heap memory total size (approximately) exceeds the calculated target heap size, the next GC cycle will start automatically. 

Note: the minimum target heap size is (GOGC * 4 / 100)MB, which is also the target heap size for the first GC cycle. 

We could use the `GODEBUG=gctrace=1` environment variable option to output a summary log line for each GC cycle. Each GC cycle summary log line is formatted like the following text (in which each # presents a number and ... means ignored messages by the current article).

`gc # @#s #%: ..., #->#-># MB, # MB goal # MB stacks # MB globals ...`

Note: in the following outputs of examples, to keep the each output line short, not all of these fields
will be shown.

Let’s use an unreal program as an example to show how to use this option:

```go
package main

import (
	"math/rand"
	"time"
)

var x [512][]*int

func garbageProducer() {
	rand.Seed(time.Now().UnixNano())
	for i := 0; ; i++ {
		n := 6 + rand.Intn(6)
		for j := range x {
			x[j] = make([]*int, 1<<n)
			for k := range x[j] {
				x[j][k] = new(int)
			}
		}
		time.Sleep(time.Second / 1000)
	}
}
func main() {
	garbageProducer()
}
```

```shell
$ GODEBUG=gctrace=1 go run main.go
gc 1 @0.018s 0%: 0.015+0.37+0.023 ms clock, 0.060+0.12/0.25/0.22+0.093 ms cpu, 3->3->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 2 @0.039s 1%: 0.23+0.74+0.003 ms clock, 0.92+0.57/0.26/0.025+0.015 ms cpu, 3->3->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 3 @0.044s 1%: 0.022+0.42+0.025 ms clock, 0.091+0.30/0.30/0.37+0.10 ms cpu, 3->3->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 4 @0.052s 1%: 0.048+0.67+0.024 ms clock, 0.19+0.13/0.42/0.24+0.099 ms cpu, 3->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
# command-line-arguments
gc 1 @0.000s 15%: 0.062+0.94+0.002 ms clock, 0.25+0.074/0.86/0.10+0.011 ms cpu, 4->6->5 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 4 P
gc 2 @0.023s 2%: 0.015+2.0+0.017 ms clock, 0.063+0.057/1.6/0.53+0.069 ms cpu, 10->11->7 MB, 12 MB goal, 0 MB stacks, 0 MB globals, 4 P
```

Here, the `#->#-># MB` and `# MB goal` fields are what we have most interests in. In a `#->#-># MB` field:

- the last number is non-garbage heap memory total size (a.k.a. live heap).
- the first number is the heap size at a GC cycle start, which should be approximately equal to
the target heap size (the number in the # MB goal field of the same line).
From the outputs, we could find that
- the live heap sizes stagger much (yes, the above program is deliberately designed as such),
so the GC cycle intervals also stagger much.
- the GC cycle frequency is so high that the percentage of time spent on GC is too high. The above outputs show the percentage varies from 8% to 12%.

The reason of the findings is that the live heap size is small (staying roughly under 5MiB) and
staggers much, but the root memory block total size is almost zero.

One way to reduce the time spent on GC is to increase the GOGC value:

```shell
$ GOGC=1000 GODEBUG=gctrace=1 go run main.go
gc 1 @0.074s 2%: ..., 38->43->15 MB, 40 MB goal, ...
gc 2 @0.810s 1%: ..., 160->163->9 MB, 167 MB goal, ...
gc 3 @1.285s 1%: ..., 105->107->11 MB, 109 MB goal, ...
gc 4 @1.835s 1%: ..., 125->128->10 MB, 129 MB goal, ...
gc 5 @2.331s 1%: ..., 114->117->18 MB, 118 MB goal, ...
gc 6 @3.250s 1%: ..., 199->201->8 MB, 204 MB goal, ...
gc 7 @3.703s 1%: ..., 96->98->18 MB, 100 MB goal, ...
gc 8 @4.580s 1%: ..., 201->204->10 MB, 207 MB goal, ...
gc 9 @5.111s 1%: ..., 118->119->3 MB, 122 MB goal, ...
gc 10 @5.306s 1%: ..., 43->43->4 MB, 44 MB goal, ....
```

From the above outputs, we could find that, after increase the `GOGC` value to 1000, GC cycle intervals
and the heap sizes at GC cycle beginning both become much larger now. But GC cycle intervals
and live heap sizes still stagger much, which might be a problem for some programs. The following
sections will introduce some ways to solve the problems.