Skip to content
Rezha Julio
Go back

Go 1.26: Stack vs Heap Allocation — Faster Code Without Changing a Line

3 min read

Keith Randall just published Allocating on the Stack on the Go blog. If you’ve ever profiled a Go service and seen a pile of tiny allocations from append growing a slice from zero, this one is worth reading.

The gist: the Go 1.25 and 1.26 compilers can now keep slice backing stores on the stack in more situations than before.

What actually changed

The improvements landed in two stages.

Go 1.25 tackled make with variable sizes. Previously, make([]T, 0, n) could only be stack-allocated if n was a compile-time constant. Now the compiler provisions a small 32-byte buffer on the stack and uses it when the requested size fits. So make([]int, 0, someVar) can avoid the heap entirely if someVar is small enough.

Go 1.26 goes further in two ways.

First, append on a nil or empty slice now uses a stack-allocated backing store as its first allocation. No make with a capacity hint needed. How much fits depends on the element size: a 32-byte buffer holds four 8-byte values, eight 4-byte values, and so on. If the slice stays small, it never touches the heap.

Second, slices that escape the function (returned to the caller) can still benefit. The compiler injects a runtime.move2heap call right before the return. If the slice already grew beyond the stack buffer, the call is a no-op. If it’s still on the stack, it copies to a single, right-sized heap allocation at the last moment. This is the part I find most interesting, because it actually beats hand-written code. A manual copy + return pattern always pays for the final allocation and copy. The compiler only does it when the slice is still stack-backed.

Why this matters in practice

I’ve worked on Go services where append-heavy hot paths showed up in allocation profiles: building slices of results from database queries, collecting validation errors, assembling middleware chains. The kind of code where the slice is usually small but you don’t want to hardcode a capacity.

The usual fix was sprinkling make([]T, 0, expectedSize) everywhere, which works but adds noise. These compiler changes make that unnecessary for the common case.

Go 1.26 also enables the Green Tea garbage collector by default (it was experimental in 1.25). The Go team reports 10-40% less GC overhead for allocation-heavy programs. Combined with fewer objects landing on the heap in the first place, the two changes reinforce each other.

The catch

If the new allocation patterns expose existing unsafe.Pointer misuse or other correctness issues, you can disable the optimization:

Terminal window
go build -gcflags=all=-d=variablemakehash=n

The bisect tool with -compile=variablemake can narrow down which allocation is causing trouble.

Bottom line

Upgrade to Go 1.26, rebuild, and check your allocation profiles. I’d be surprised if you don’t see fewer heap allocations.

The full blog post with code examples: Allocating on the Stack.


Related Posts


Previous Post
Sharpen the Saw: Don't Debug in the Dark