notexOPENOverview
Notex/Languages/Allocating Slices
04.16LESSON 16 OF 20 · 3 MIN READ

Allocating Slices

Always free to read

One of the benefits of garbage-collected languages like Go is that new versions can improve memory allocation. Your code can take advantage of these improvements with a simple recompile.

In the earliest versions of Go, all slice storage allocations happened on the heap. Starting way back in Go 1.2 in 2013, this behavior was modified. If you declared a slice with a constant capacity, the value of (capacity * type size) was less than 64KB, and the slice didn’t need to escape to the heap, the slice’s storage space would be allocated on the stack. This was helpful for statements like mySlice := make([]int, 20). However, if you used a variable in make (mySlice := make([]int, sliceSize)) or wrote var mySlice []int, the slice’s data was still allocated on the heap.

Go 1.25 and 1.26 improved this behavior. Now, if you declare a slice with make and use a variable for the capacity (or the length, if no capacity is specified), the Go complier calculates how many instances of the value will fit into 32 bytes and inserts code that checks if the variable’s value is greater than that number. If it is, the slice is allocated on the heap. If not, the slice is allocated on the stack within a 32-byte buffer.

For example, on a 64-bit architecture, the size of an int is 8 bytes. That means four int instances will fit into 32 bytes. If there is a statement mySlice := make([]int, 0, sliceSize) and sliceSize is larger than 4, the slice is allocated on the heap. But if sliceSize is between 0 and 4 (inclusive), a 32-byte buffer is created on the stack and the slice is allocated within that buffer. If the slice’s capacity outgrows the 32-byte buffer, the slice is reallocated on the heap. The diagram in Figure 5-13 shows this behavior:

Choosing to allocate a slice on the stack or the heap

Figure 5-13. Choosing to allocate a slice on the stack or the heap

The compiler also gives each append statement its own 32-byte byte buffer on the stack. When append is called, it checks if the passed-in slice has zero capacity. If it does, append puts the slice’s data in its on-stack buffer. Future appends will grow the slice within this buffer. If the slice outgrows the buffer, it is reallocated on the heap.

Note

Why doesn’t the Go compiler generate code that keeps allocating the slice on the stack? It’s because the size of a individual stack frame in Go must be known at compile time. The stack as a whole can grow, but each individual stack frame that makes up the stack has a constant size. Given this, some value had to be picked for the on-stack buffer size; it is currently 32 bytes.

When a function returns a slice, the backing data must be allocated on the heap. Otherwise, the data would be invalidated when the function exits and the stack frame would be destroyed. Go 1.26 included an additional optimization to address situations where a slice needs to be allocated on the heap solely because it is returned from the function that declared it. In that case, the runtime allocates the slice’s storage on the stack and copies it to heap allocation automatically when the function exits. These modifications keep intermediate data for small slices on the stack, further reducing pressure on the garbage collector. For more details on these optimizations, read the Go team’s blog post.

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

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