Skip to content
Tech News
← Back to articles

Size-Specialized Memory Allocation

read original more articles
Why This Matters

Go 1.27 introduces size-specialized memory allocation functions for small objects (80 bytes or fewer), speeding up allocations by 20-30% and boosting overall program performance by up to 1%. This matters because faster memory allocation in a widely-used systems language like Go can meaningfully improve efficiency across countless backend services, cloud infrastructure, and developer tools without requiring any code changes from programmers.

Key Takeaways

Michael Matloob

16 September 2026

Go 1.27 includes faster memory allocation for allocations of 80 bytes or fewer. Allocations can be up to 20-30% faster, making allocation-heavy programs up to 1% faster. The Go runtime improves the performance of those allocations by adding specialized functions that are used to allocate certain sizes. These specialized functions can then make certain assumptions that make them faster and easier to optimize. This blog post will explain how this works and how it makes your programs faster.

Heap allocations are created by the runtime’s mallocgc function, which requires the size of the allocation and whether it contains pointers. When the compiler determines that an object escapes to the heap or otherwise needs to be dynamically allocated, it inserts a call to newobject , which is a simple wrapper function that extracts the size of the object and whether it contains pointers and passes those to mallocgc .

These two pieces of information determine most of the work that the allocator needs to do. The size is important because the allocator defines ranges of sizes called “size classes”. For most allocations that are not too large and not too small, it will return a block of memory from a list of free objects that are all sized to the maximum size of the size class. So, for instance, size class 3 is 17-24 bytes. So whether you allocate 17 bytes or 24 bytes, the allocator will give you the next free 24 byte object available in its list of 24 byte objects. Below is a table of the size ranges of each of the size classes up to 80 bytes. There are separate sets of free lists, which we call spans, depending on whether the allocation has pointers, because of the bookkeeping we need to do for the garbage collector.

Size class Range of sizes 1 1-8 bytes 2 9-16 bytes 3 17-24 bytes 4 25-32 bytes 5 33-48 bytes 6 49-64 bytes 7 65-80 bytes

Because the size class and whether the allocation contains pointers determine which span we allocate from, the allocator has a type called the span class, whose value encodes both: it’s defined as sizeClass<<1 | noPointers . When implementing size-specialized malloc it was clear that it made the most sense to specialize on these span classes because much of the behavior of the allocator was determined by the span class.

We generate a specialized mallocgc variant for every span class. For instance, the function that allocates pointer-free objects in size class 3 is called mallocgcSmallNoScanSC3 . Small means not tiny or large, NoScan means it has no pointers, and SC3 means size class 3 (17-24 bytes). Specialized functions are designed to be as simple as possible and cannot handle every corner case during allocation. When they detect such a case, such as when the GC is active, they fall back to a more generic allocation routine.

So we end up with a new specialized mallocgc function for the tiny case (always no pointers) and one for each non-tiny span class: the non-pointer spans of size class 2 and above, and the pointer spans of size class 1 and above. These specialized mallocgc variants themselves are faster than calling mallocgc , but the benefits of specializing decrease as the sizes get larger: the allocation often gets dominated by needing to clear the memory, and at some point the rest of the work of allocation becomes negligible. And it’s not enough to just be faster than mallocgc ! With size-specialized allocation, when the compiler knows which span class it needs to allocate for, it can directly insert a call to the specialized function instead of the call to newobject . But the compiler often doesn’t know which span class is being allocated when code is generated: think slices of dynamic length. Since the compiler can’t determine the size of the allocation, it will keep the call to mallocgc , and mallocgc itself has to determine whether a specialized function is available, which one it is, and then needs to call it. So the performance of the specialized function has to be high enough that even with the overhead of a dynamic call it’s still faster.

This overhead issue isn’t the only problem. If it were, then we could create larger specialized functions, and have them reserved for the compiler to insert them when the size is known at compile-time. And if we didn’t call them dynamically we wouldn’t have to pay the dynamic overhead. But each specialized function that’s added increases the sizes of the executables produced by the compiler, and, more importantly, takes up precious instruction cache space. The single mallocgc function is often in the instruction cache because of how frequently allocations happen. If we have too many specialized functions and they’re not available in the cache, the overhead to retrieve the specialized code into the cache can cancel out any benefits. And the more specialized allocation code that’s in the icache, the more it crowds out the user code, making it slower to fetch and run user code. Doing a bunch of benchmarks stopping at different size classes, we determined that stopping at 80 bytes was the sweet spot.

... continue reading