Allocation on the heap

The memory management is designed to be fast in a concurrent environment and integrated with the garbage collector. Let’s start with a simple example:

package main

type smallStruct struct {
   a, b int64
   c, d float64
}

func main() {
   smallAllocation()
}

//go:noinline
func smallAllocation() *smallStruct {
   return &smallStruct{}
}

The annotation //go:noinline will disable in-lining that would optimize the code by removing the function and, therefore, end up with no allocation.

Running the escape analysis command with go tool compile "-m" main.go will confirm the allocation made by Go:

main.go:14:9: &smallStruct literal escapes to heap

Dumping the assembly code for this program, thanks to go tool compile -S main.go, would also explicitly show us the allocation:

0x001d 00029 (main.go:14)   LEAQ   type."".smallStruct(SB), AX
0x0024 00036 (main.go:14)  PCDATA $0, $0
0x0024 00036 (main.go:14)  MOVQ   AX, (SP)
0x0028 00040 (main.go:14)  CALL   runtime.newobject(SB)

The function newobject is the built-in function for new allocations and proxy mallocgc, a function that manages them on the heap. There are two strategies in Go, one for the small allocations and one for larger ones.

Small allocation

For the small allocations, under 32kb, Go will try to get the memory from a local cache called mcache. This cache handles a list of span, called mspan, that contains the memory available for allocation:

Each thread M is assigned to a processor P and handles at most one goroutine at a time. While allocating memory, our current goroutine will use the local cache of its current M to find the first free object available in the span list. Using this local cache does not require lock and makes the allocation more efficient.

The span list is divided into ~70 size classes, from 8 bytes to 32k bytes, that can store different object sizes:

Each span exists twice: one list for objects that do not contain pointer and another one that contains pointer. This distinction will make the life of the garbage collector easier since it will not have to scan the spans that do not contain any pointer.

In our previous example, the size of the structure is 32 bytes and will fit in the 32 bytes span:

Now, we may wonder what would happen if the span does not have a free slot during the allocation. Go maintains central lists of spans per size classes, called mcentral, with the spans that contain free objects and the ones that do not:

mcentral maintains a double linked list of spans; each of them has a reference to the previous span and next span. A span in the empty list could contain some memory in-use already. Indeed, when the garbage collector sweeps the memory, it could clean a part of the span that would be put back in the empty list of some slots that have been cleaned.

Our program can now request a span from the central list if it runs out of slots:

Go needs a way to get new spans to the central list if none are available in the empty list. New spans will now be allocated from the heap and linked to the central list:

The heap pulls the memory from the OS when needed. If it needs more memory, the heap will allocate a large chunk of memory, called arena, of 64Mb for the 64bits architectures and 4Mb for most of the other architectures. The arena also maps the memory page with the spans:

Large allocation

Go does not manage the large allocations with a local cache. Those allocations, greater than 32kb, are rounded up to the page size and the pages are allocated directly to the heap.

Big picture

We now have a good view of what is happening at a high level during the memory allocation. Let’s draw all the components together to get the full picture:

We now have a good view of what is happening at a high level during the memory allocation. Let’s draw all the components together to get the full picture:

发表评论

电子邮件地址不会被公开。 必填项已用*标注