blob: e67ab396303ecb64863b2cc75e637375ee2d6fb6 [file] [log] [blame] [view]
---
title: "Go runtime: 4 years later"
date: 2022-09-26
by:
- Michael Knyszek
summary: A check-in on the status of Go runtime development
---
Since our [last blog post about the Go GC in 2018](/blog/ismmkeynote) the
Go GC, and the Go runtime more broadly, has been steadily improving.
We've tackled some large projects, motivated by real-world Go programs and real
challenges facing Go users.
Let's catch you up on the highlights!
### What's new?
- `sync.Pool`, a GC-aware tool for reusing memory, has a [lower latency
impact](/cl/166960) and [recycles memory much more
effectively](/cl/166961) than before.
(Go 1.13)
- The Go runtime returns unneeded memory back to the operating system [much
more proactively](/issue/30333), reducing excess memory
consumption and the chance of out-of-memory errors.
This reduces idle memory consumption by up to 20%.
(Go 1.13 and 1.14)
- The Go runtime is able to preempt goroutines more readily in many cases,
reducing stop-the-world latencies up to 90%.
[Watch the talk from Gophercon
2020 here.](https://www.youtube.com/watch?v=1I1WmeSjRSw)
(Go 1.14)
- The Go runtime [manages timers more efficiently than
before](/cl/171883), especially on machines with many CPU cores.
(Go 1.14)
- Function calls that have been deferred with the `defer` statement now cost as
little as a regular function call in most cases.
[Watch the talk from Gophercon 2020
here.](https://www.youtube.com/watch?v=DHVeUsrKcbM)
(Go 1.14)
- The memory allocator's slow path [scales](/issue/35112)
[better](/issue/37487) with CPU cores, increasing throughput up
to 10% and decreasing tail latencies up to 30%, especially in highly-parallel
programs.
(Go 1.14 and 1.15)
- Go memory statistics are now accessible in a more granular, flexible, and
efficient API, the [runtime/metrics](https://pkg.go.dev/runtime/metrics)
package.
This reduces latency of obtaining runtime statistics by two orders of
magnitude (milliseconds to microseconds).
(Go 1.16)
- The Go scheduler spends up to [30% less CPU time spinning to find new
work](/issue/43997).
(Go 1.17)
- Go code now follows a [register-based calling
convention](/issues/40724) on amd64, arm64, and ppc64, improving
CPU efficiency by up to 15%.
(Go 1.17 and Go 1.18)
- The Go GC's internal accounting and scheduling has been
[redesigned](/issue/44167), resolving a variety of long-standing
issues related to efficiency and robustness.
This results in a significant decrease in application tail latency (up to 66%)
for applications where goroutines stacks are a substantial portion of memory
use.
(Go 1.18)
- The Go GC now limits [its own CPU use when the application is
idle](/issue/44163).
This results in 75% lower CPU utilization during a GC cycle in very idle
applications, reducing CPU spikes that can confuse job shapers.
(Go 1.19)
These changes have been mostly invisible to users: the Go code they've come to
know and love runs better, just by upgrading Go.
### A new knob
With Go 1.19 comes an long-requested feature that requires a little extra work
to use, but carries a lot of potential: [the Go runtime's soft memory
limit](https://pkg.go.dev/runtime/debug#SetMemoryLimit).
For years, the Go GC has had only one tuning parameter: `GOGC`.
`GOGC` lets the user adjust [the trade-off between CPU overhead and memory
overhead made by the Go GC](https://pkg.go.dev/runtime/debug#SetGCPercent).
For years, this "knob" has served the Go community well, capturing a wide
variety of use-cases.
The Go runtime team has been reluctant to add new knobs to the Go runtime,
with good reason: every new knob represents a new _dimension_ in the space of
configurations that we need to test and maintain, potentially forever.
The proliferation of knobs also places a burden on Go developers to understand
and use them effectively, which becomes more difficult with more knobs.
Hence, the Go runtime has always leaned into behaving reasonably with minimal
configuration.
So why add a memory limit knob?
Memory is not as fungible as CPU time.
With CPU time, there's always more of it in the future, if you just wait a bit.
But with memory, there's a limit to what you have.
The memory limit solves two problems.
The first is that when the peak memory use of an application is unpredictable,
`GOGC` alone offers virtually no protection from running out of memory.
With just `GOGC`, the Go runtime is simply unaware of how much memory it has
available to it.
Setting a memory limit enables the runtime to be robust against transient,
recoverable load spikes by making it aware of when it needs to work harder to
reduce memory overhead.
The second is that to avoid out-of-memory errors without using the memory limit,
`GOGC` must be tuned according to peak memory, resulting in higher GC CPU
overheads to maintain low memory overheads, even when the application is not at
peak memory use and there is plenty of memory available.
This is especially relevant in our containerized world, where programs are
placed in boxes with specific and isolated memory reservations; we might as
well make use of them!
By offering protection from load spikes, setting a memory limit allows for
`GOGC` to be tuned much more aggressively with respect to CPU overheads.
The memory limit is designed to be easy to adopt and robust.
For example, it's a limit on the whole memory footprint of the Go parts of an
application, not just the Go heap, so users don't have to worry about accounting
for Go runtime overheads.
The runtime also adjusts its memory scavenging policy in response to the memory
limit so it returns memory to the OS more proactively in response to memory
pressure.
But while the memory limit is a powerful tool, it must still be used with some
care.
One big caveat is that it opens up your program to GC thrashing: a state in
which a program spends too much time running the GC, resulting in not enough
time spent making meaningful progress.
For example, a Go program might thrash if the memory limit is set too low for
how much memory the program actually needs.
GC thrashing is something that was unlikely previously, unless `GOGC` was
explicitly tuned heavily in favor of memory use.
We chose to favor running out of memory over thrashing, so as a mitigation, the
runtime will limit the GC to 50% of total CPU time, even if this means exceeding
the memory limit.
All of this is a lot to consider, so as a part of this work, we released [a
shiny new GC guide](/doc/gc-guide), complete with interactive visualizations to
help you understand GC costs and how to manipulate them.
### Conclusion
Try out the memory limit!
Use it in production!
Read the [GC guide](/doc/gc-guide)!
We're always looking for feedback on how to improve Go, but it also helps to
hear about when it just works for you.
[Send us feedback](https://groups.google.com/g/golang-dev)!