All computing environments must deal with memory management. This article discusses some memory management concepts used by the Go programming language. This article is written for programmers familiar with basic memory management concepts but unfamiliar with Go memory management in particular.
Stack, Heap, and Fixed Size Segments
For the purposes of this article, there are three ways to allocate memory: the stack, the heap, and fixed size segments.
Stack
The stack has a top that moves up and down. Space is allocated on the stack by moving the top up (i.e. pushing items on the stack) and space is deallocated by moving the top down (i.e. popping items off the stack). The top is an address that can be incremented and decremented with fast arithmetic operations.
Typically, a functions parameters and local variables are allocated on the stack.
Each goroutine has its own stack; thus, no synchronization (e.g., locking) is necessary.
Goroutine stacks are allocated on the heap. If the stack needs to grow beyond the amount allocated for it, then heap operations (allocate new, copy old to new, free old) will occur.
Heap
Unlike the stack, the heap does not have a single partition of allocated and free regions. Rather, there is a set of of free regions. A data structure must be used to implement this set of free regions. When an item is allocated, it is removed from the free regions. When an item is freed, it is added back to the set of free regions.
Unlike the stack, the heap is not owned by one goroutine, so manipulating the set of free regions in the heap requires synchronization (e.g., locking).
Fixed Sized Segments
Memory can also be allocated in one of the fixed sized segments, such as the data segment and code segment. Fixed sized segments are defined at compile time and do not change size at runtime. Read-write fixed size segments (e.g., the data segment) contain global variables while read-only segments (e.g., code segment and rodata segment) contain constant values and instructions.1
What Goes Where?
The Go Programming Language Specification does not define
where items will be allocated. For example, a variable defined as var x int
could be allocated
on the stack or the heap and still follow the language spec. Likewise, the integer pointed
to by p in p := new(int)
could be allocated on the stack or the heap.
However, certain requirements will exclude some choices of memory in certain conditions. For instance:
- The size of the data segment cannot change at run time, and therefore cannot be used for data structures that change size.
- The lifetime of items in the stack are ordered by their position on the stack. If the top of the stack is address X then everything above X will be deallocated while everything below X will remain allocated. Memory allocated by a function can escape that function if referenced by an item outside the scope of the function and therefore cannot be allocated on the stack (because it’s still being referenced), and neither can it be allocated in the data segment (because the data segment cannot grow at runtime), thus it must be allocated on the heap – although inlining can remove some of these heap allocations.
Escape Analysis
Escape analysis is used to determine whether an item can be allocated on the stack. It determines if an item created in a function (e.g., a local variable) can escape out of that function or to other goroutines. For example, in the following function, x escapes from the function that defines it:
package escapeanalysis
func Foo() *int {
var x int
return &x
}
Items that escape must be allocated on the heap. Thus x would be allocated on the heap.2
The exact escape analysis algorithm can change between Go versions. However, you can
use go tool compile -m
to print optimization decisions, which include the escape analysis. For example, on the previous
program with Go version 1.5.2, you get the following output:
escape.go:3: can inline Foo
escape.go:4: moved to heap: x
escape.go:5: &x escapes to heap
Garbage Collector
Go uses garbage collection for memory management. The Go garbage collector occasionally has to stop the world to complete the collection task. Since Go version 1.5, the collector is designed so that the stop the world task will take no more than 10 milliseconds out of every 50 milliseconds of execution time.
The garbage collector has to be aware of both heap and stack allocated items. This is easy to see if you consider a heap allocated item, H, referenced by a stack allocated item, S. Clearly, the garbage collector cannot free H until S is freed and so the garbage collector must be aware of lifetime of S, the stack allocated item.
Performance
If your process is CPU bound, use runtime/pprof package and go tool pprof
to profile your program. If you see symbols like growslice and newobject taking up a lot of time, optimizing memory allocations may improve performance.
Assuming you’ve determined optimizing memory use would improve performance of your program, then reduce the number of allocations – especially heap allocations.
- Reuse memory you’ve already allocated.
- Restructure your code so the compiler can make stack allocations instead of heap allocations. Use
go tool compile -m
to help you identify escaped variables that will be heap allocated and then rewrite your code so that they can be stack allocated. - Restructure your CPU bound code to pre-allocate memory in a few big chunks rather than continuously allocating small chunks.
References
- Go 1.4+ Garbage Collection (GC) Plan and Roadmap – 2014-8-6
- Go Escape Analysis Flaws – 2015-2-10
- Golang Escape Analysis – 2015-11-11
- Profiling Go Programs – 2011-6-24
- The Go Programming Language Specification