· via Hacker News – Front Page (native)
Go 1.27 speeds up small allocations by up to 30% with specialized malloc paths
Go 1.27 replaces the single generic allocation path with specialized functions for requests of 80 bytes or less, cutting allocation cost by 20-30% and lifting allocation-heavy programs by up to 1%.

Go 1.27 includes a reworked allocation path in its runtime that makes allocations of 80 bytes or smaller up to 20-30% faster, with allocation-heavy programs gaining as much as 1% overall, according to a post on the official Go blog. The change is invisible to programmers, with no new API and no build flags, but it touches one of the hottest code paths in the language.
How allocation worked before
Every heap allocation in Go flows through a runtime function called mallocgc, which needs two pieces of information: the size of the request and whether the object contains pointers. The compiler wraps most of these calls in newobject, a thin shim that extracts those facts from type information and forwards them along.
The allocator sorts sizes into buckets called size classes, such as 17-24 bytes or 65-80 bytes, and serves requests from free lists of blocks sized to the top of the bucket. These free lists, called spans, are kept separate for pointer-containing and pointer-free objects because the garbage collector needs different bookkeeping for each. The combination of size class and pointer-ness is encoded in a span class, and that value drives most of the allocator's decisions.
One specialized function per span class
Instead of a single general-purpose mallocgc, Go 1.27 generates a dedicated variant for each span class up to 80 bytes, plus one for tiny allocations. The variant that handles pointer-free objects of 17-24 bytes, for example, is named mallocgcSmallNoScanSC3.
Because each variant handles exactly one size bucket and pointer configuration, it can skip work the generic path must do: computing the span class, branching on pointer-ness, and calling out to memory-clearing helpers. When a specialized function encounters a case it cannot handle, an active garbage collection being one example, it falls back to the generic routine.
The compiler can call these variants directly when the allocation size is known at compile time. When it is not, as with slices of dynamic length, mallocgc remains in place and dispatches to the right specialized function at runtime. That means the specialized code has to be fast enough to win even after paying for the dynamic dispatch.
Why the cutoff is 80 bytes
Adding variants is not free. Each one grows the binary and, more importantly, competes for instruction cache. mallocgc is almost always resident in the icache precisely because allocation happens so often; a crowd of specialized functions risks evicting each other and crowding out user code, erasing the gains. The Go team benchmarked cutoffs at different size classes and found 80 bytes to be the sweet spot. The benefit also shrinks as sizes grow, because zeroing the memory comes to dominate the allocation and the surrounding work becomes negligible.
Generated code, not hand-written copies
Dozens of near-identical hand-written functions would drift out of sync over time. Instead, the shared logic lives in ordinary, type-checked Go that mostly serves as stubs, and an inliner built on the standard library's go/ast package and the golang.org/x/tools/go/ast/astutil package stitches the pieces into each variant. That approach lets the team aggressively inline helpers the compiler would normally skip, since the compiler declines to inline functions it considers too large, and to move rare cases such as runtime debugging flags into slow-path functions so the hot code stays small.
The single largest win, per the post, comes from memory clearing. Freshly allocated memory often must be zeroed, and that clear can take up most of the allocation's time. In a specialized function the clear size is a compile-time constant, so the generated code can emit the clearing instructions directly rather than calling the memclrNoHeapPointers helper, skipping a function call and several branches. That difference matters most for very small allocations and fades as sizes increase. Constant sizes also let the compiler speed up bookkeeping, such as marking where pointers live in the allocated memory.
Why it matters
Allocation speed is a tax every Go program pays, and small objects dominate the workload: the post highlights 16- and 24-byte allocations as especially common sizes that benefit the most. A 20-30% reduction in allocation cost translating into up to 1% end-to-end improvement is a meaningful, zero-effort upgrade for a mature runtime. Beyond the raw numbers, the work is a case study in how runtimes can trade binary size and instruction cache pressure for per-call speed through code generation, and it establishes a pattern that other hot runtime paths could follow in future releases.
- #go
- #runtime
- #performance
- #memory-management
- #compilers