A visual guide to Go Memory Allocator from scratch (Golang)
By: Ankur Anand
Tags: Memory Allocation, Featured, Computer Science, Golang, Go

Go Memory Allocator

We Know Go Runtime schedules Goroutines (G) onto Logical Processors (P) for execution. Likewise, TCMalloc Go also divides Memory Pages into a block of 67 different classes Size.

If you’re not familiar with the Go scheduler you can get an overview (Go scheduler: Ms, Ps & Gs), till then I will wait for you over here.

Size Classes in Go

As Go manages pages at the granularity of 8192B if this page is divided into a block size of 1kB we get a total of 8 such blocks within that page for example.

8 KB page divided into a size class of 1KB (In Go pages are maintained at the granularity of 8KB)

These run’s of pages in Go is also managed through a structure known as mspan.

The size classes and page count(run of pages gets chopped into objects of the given size) that gets allocated to each size classes are chosen so that rounding an allocation request up to the next size class wastes at most 12.5%

mspan

Simply put, it ’s a double linked list object that contains the start address of the page, span class of the page that it has, and the number of pages it contains.

Illustrative Representation of a mspan in Go memory allocator

mcache

Like TCMalloc Go provides each Logical Processors(P) a Local Thread Cache of Memory known as mcache, so that if Goroutine needs memory it can directly get it from the mcache without any locks being involved as at any point of time only one Goroutine will be running on Logical Processors(P).

mcache contains a mspan of all class size as a cache.

Illustrative Representation of a Relationship between P, mcache, and mspan in Go.

As there is mcache Per-P, so no need to hold locks when allocating from the mcache.

For each class size, there are two types.

scan — Object that contains a pointer. noscan — Object that doesn’t contains a pointer.

One benefits of this approach being while doing Garbage Collection, noscan object doesn’t need to be traversed to find any containing live object.

What Goes to mcache ?.

Object size <=32K byte are allocated directly to mcache using the corresponding size class mspan.

What happens When mcache has no free slot?

A new mspan is obtained from the mcentral list of mspans of the required size class.

mcentral

mcentral Object collects all spans of a given size class and each mcentral is two lists of mspans.

empty mspanList — List of mspans with no free objects or mspans that has is cached in an mcache. nonempty mspanList — List of spans with a free object.

When a new Span is requested from mcentral, it’s taken (if available) from the nonempty list of mspanList. The relationship between these two lists is as follow When a new span is requested, the request is fulfilled from the non-empty list and that span is put into the empty list. When the span is freed then based on the number of free objects in the span it is put back to the non-empty list.

Illustrative Representation of a mcentral

Each mcentral structure is maintained inside the mheap structure.

mheap

mheap is the Object that manages the heap in Go, only one global. It own the virtual addresses space.

Illustrative Representation of a mheap.

As we can see from the above illustration mheap has an array of mcentral. This array contains mcentral of each span class.

central [numSpanClasses]struct {

mcentral mcentral

pad [sys.CacheLineSize unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte

}

Since We have mcentral for each span class, when a mspan is requested by mcache from mcentral, the lock is involved at individual mcentral level, so any other mcache requesting a mspan of different size at the same time can also be served.

Padding makes sure that the MCentrals are spaced CacheLineSize bytes apart so that each MCentral.lock gets its own cache line in order to avoid false sharing problems.

So what happens when this mcentral list is empty? mcentral obtains a run of pages from the mheap to use for spans of the required size class.

free[_MaxMHeapList]mSpanList : This is a spanList array. The mspan in each spanList consists of 1 ~ 127 (_MaxMHeapList — 1) pages. For example, free[3] is a linked list of mspans containing 3 pages. Free means free list, which is unallocated. Corresponding to the busy list.

: This is a spanList array. The in each spanList consists of 1 ~ 127 (_MaxMHeapList — 1) pages. For example, free[3] is a linked list of containing 3 pages. Free means free list, which is unallocated. Corresponding to the busy list. freelarge mSpanList: A list of mspans. The number of pages per element (that is, mspan) is greater than 127. It’s maintained as a mtreap Data structure. Corresponding to busylarge.

Object of Size > 32k, is a large object, allocated directly from mheap. These large request comes at an expenses of central lock, so only one P’s request can be served at any given point of time.

Object allocation Flow

• Size > 32k is a large object, allocated directly from mheap.

• Size < 16B, using mcache’s tiny allocator allocation

• Size between 16B ~ 32k, calculate the sizeClass to be used and then use the block allocation of the corresponding sizeClass in mcache

• If the sizeClass corresponding to mcache has no available blocks, apply to mcentral.

• If there are no blocks available for mcentral, apply to mheap and use BestFit to find the most suitable mspan. If the application size is exceeded, it will be divided as needed to return the number of pages the user needs. The remaining pages constitute a new mspan, and the mheap free list is returned.

• If there is no span available for mheap, apply to the operating system for a new set of pages (at least 1MB).

But Go allocates pages in even large size (called arena) at OS Level. Allocating a large run of pages amortizes the cost of talking to the operating system.

All memory requested on the heap comes from the arena. Let’s look at what this arena looks like.

Go Virtual Memory

Lets us look into the memory of simple go program.

func main() {

for {}

}

process stats for a program

So even for a simple go program virtual Space is around ~100 MB while RSS is just 696kB . Lets us try to figure out this difference first.

map and smap stats.

So there are regions of memory which are sized around ~ 2MB, 64MB and 32MB . What are these?

Arena

It turns out the virtual memory layout in go consists of a set of arenas. The initial heap mapping is one arena i.e 64MB (based on go 1.11.5).

current incremental arena size on a different system.

So currently memory is mapped in small increments as our program needs it, and it starts with one arena (~64MB).

Please take these number with a grain of salt. Subject to change. Earlier go used to reserve a continuous virtual address upfront, on a 64-bit system the arena size was 512 GB. (what happens if allocations are large enough and are rejected by mmap ?)

This set of arenas is what we call heap. In Go, each arena is managed at a granularity of 8192 B of pages.

Single arena ( 64 MB ).

Go also has two more blocks which span and bitmap. Both of them are allocated off-heap and contains metadata of each arena. It’s mostly used during Garbage Collection (so we will leave it for now).