notexOPENOverview
Notex/Languages/Reducing the Garbage Collector’s Workload
04.14LESSON 14 OF 20 · 8 MIN READ

Reducing the Garbage Collector’s Workload

Always free to read

Using buffers is just one example of how you can reduce the garbage collector’s workload. When programmers talk about “garbage,” what they mean is “data that has no more pointers pointing to it.” Once there are no more pointers to that data, the memory it takes up can be reused. If memory wasn’t always being recovered, the program’s memory usage would continue to grow until the computer ran out of RAM. The garbage collector’s job is to automatically detect unused memory and recover it so it can be reused. It’s fantastic that Go has a garbage collector, because decades of experience have shown that it is very difficult for people to properly manage memory manually. But just because you have a garbage collector doesn’t mean you should create lots of garbage.

If you’ve spent time learning how programming languages are implemented, you’ve probably learned about the stack and the heap. Let’s look at both in turn.

If you’re unfamiliar, a stack is a consecutive block of memory. Every function call in a thread of execution shares the same stack. Allocating memory on the stack is fast and simple. A stack pointer tracks the last location where memory was allocated. To allocate additional memory, you change the value of the stack pointer. When you invoke a function, a new stack frame (the section of the stack that holds local variables during a function call) is created for that function’s data.

Local variables are stored on the stack, along with parameters passed into the function. Each new variable moves the stack pointer by the size of that variable’s value. When the function exits, its return values are copied back to the calling function via the stack, and the stack pointer moves back to the beginning of the stack frame for the exited function, deallocating all the stack memory used by that function’s local variables and parameters.

Note

Registers are small sets of very-high-speed memory that live directly on the CPU. Since version 1.17, Go has used a combination of registers and the stack to pass values into and out of functions. It’s faster and more complicated than stack-only function calls, but the general concepts of stack-only function calls still apply.

To store something on the stack, you have to know exactly how big it is at compile time. Go’s value types(primitive values, arrays, and structs) all have one thing in common: you know exactly how much memory they take at compile time. This is why size is considered part of an array’s type: because their sizes are known, they can be allocated on the stack instead of the heap. The size of a pointer type is also known, and it is also stored on the stack.

Note

Go is unusual in that it can increase the size of a stack while a program is running. This is possible because each goroutine has its own stack, and goroutines are managed by the Go runtime, not the underlying operating system. This has advantages: Go stacks start small and use less memory. It also has disadvantages: when the stack needs to grow, all data on the stack needs to be copied, which is slow. It’s also possible to write worst-case-scenario code that causes the stack to grow and shrink over and over.

The rules are more complicated when it comes to the data to which the pointer points. In order for Go to allocate that data on the stack, several conditions must be true:

  • The data must be a local variable whose data size is known at compile time.
  • The pointer cannot be returned from the function.
  • If the pointer is passed into a function, the compiler must be able to ensure that these conditions still hold.
  • If the size isn’t known, you can’t make space for it by moving the stack pointer.
  • If the pointer variable is returned, the memory that the pointer points to will no longer be valid when the function exits.

When the compiler determines that the data can’t be stored on the stack, we say that the data the pointer points to escapes the stack, and the compiler stores the data on the heap.

The heap is the memory managed by the garbage collector (or by hand, in languages like C and C++). I won’t discuss the details of implementing a garbage-collector algorithm, but it’s much more complicated than moving a stack pointer. Any data that’s stored on the heap is valid, as long as it can be tracked back to a pointer-type variable on a stack. Once there are no more variables on the stack pointing to that data, either directly or via a chain of pointers, the data becomes garbage and the garbage collector must clear it out. See The Go Playground for a program that demonstrates when data on the heap becomes garbage.

Note

A common source of bugs in C programs is returning a pointer to a local variable. In C, this results in the pointer pointing to invalid memory. The Go compiler is smarter: When it sees that a pointer to a local variable is returned, it stores the local variable’s value on the heap.

The Go compiler performs escape analysis, which is the process of figuring out which data can be safely stored on the stack and which data must “escape” to the heap. This algorithm isn’t perfect, but it has improved with newer releases. In some cases, data that could be stored on the stack escapes to the heap. However, the compiler has to be conservative; it can’t take the chance of leaving a value on the stack when it might need to be on the heap, because leaving a reference to invalid data causes memory corruption.

You might be wondering: What’s so bad about storing things on the heap? There are two performance-related problems that arise. First, the garbage collector takes time to do its work. It isn’t trivial to keep track of all available chunks of free memory on the heap, or to track which used blocks of memory still have valid pointers. That time is taken away from the processing that your program is written to do.

There are many garbage-collection algorithms, and they can be placed into two rough categories: those designed to maximize throughput (find the most garbage possible in a single scan) and those designed to minimize latency (finish the garbage scan as quickly as possible). Jeffrey Dean, the genius behind many of Google’s engineering successes, cowrote a paper in 2013 called “The Tail at Scale” that argues for optimizing systems for latency to keep response times low. The Go runtime’s garbage collector favors low latency. Each garbage-collection cycle is designed to “stop the world” (pause your program) for fewer than 500 microseconds. (In 2025, Dolt analyzed stop-the-world pauses in its own database and found that they averaged between 20 and 350 microseconds.) However, if your Go program creates lots of garbage, the garbage collector won’t be able to find it all during a cycle. This slows the collector and increases memory usage.

Note

Go 1.26 introduced a new garbage collection algorithm called Green Tea that reduces the time spent on garbage collection. To learn more, see the Go team’s blog post or watch the video introducing it. For information on the history and implementation of garbage collection in Go, listen to Rick Hudson’s 2018 talk at the International Symposium on Memory Management.

The second problem with heap storage deals with the nature of computer hardware. RAM might mean “random access memory,” but the fastest way to read from memory is sequentially. A slice of structs in Go lays out all the data sequentially in memory, making it fast to load and process. By contrast, the data from a slice of pointers to structs (or structs whose fields are pointers) is scattered across RAM, making it far slower to read and process. Forrest Smith, in an in-depth blog post, explores how much this can affect performance: his numbers indicate that it’s roughly two orders of magnitude slower to access data via pointers stored randomly in RAM.

This approach—​writing software that’s aware of the hardware it’s running on—​is called mechanical sympathy. The term comes from the world of car racing, where the idea is that a driver who understands what the car is doing can best squeeze the maximum performance out of it. In 2011, Martin Thompson began applying the term to software development. Following best practices in Go gives you mechanical sympathy automatically.

Compare Go’s approach to Java’s: In Java, local variables and parameters are stored in the stack, just as in Go. However, as discussed earlier, objects in Java are implemented as pointers. Only the pointer to each object variable instance is allocated on the stack; the data within the object is allocated on the heap. Only primitive values (numbers, Booleans, and chars) are stored entirely on the stack. This means that the Java garbage collector has to do a great deal of work. It also means that implementations of the List interface in Java are built using a pointer to an array of pointers. Even though they look like linear data structures, reading from them actually involves bouncing through memory, which is highly inefficient. The sequential data types in Python, Ruby, and JavaScript behave similarly. To work around all this inefficiency, the Java Virtual Machine includes some very clever garbage collectors that do lots of work. Some are optimized for throughput, some for latency, and all come with configuration settings to tune them for the best performance. The virtual machines for Python, Ruby, and JavaScript are less optimized, and their performance suffers accordingly.

Now you can see why Go encourages you to use pointers sparingly. You can reduce the garbage collector’s workload by making sure to store as much as possible on the stack. Slices of structs or primitive types have their data lined up sequentially in memory for rapid access. And when the garbage collector works, it is optimized to return quickly rather than gathering the most garbage. The key to making this approach work is to simply create less garbage in the first place. While focusing on optimizing memory allocations can feel like premature optimization, the idiomatic approach in Go is also the most efficient.

If you want to learn more about heap versus stack allocation and escape analysis in Go, see the excellent blog posts by Bill Kennedy of Ardan Labs and Achille Roussel and Rick Branson of Segment.

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K