> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Go 1.26: Memory Allocation Optimization Analysis
- URL: https://huizhou92.com/go-1-26-memory-allocation-optimization-analysis/
- Published: 2026-01-06T07:13:49.000Z
- Updated: 2026-01-06T07:13:49.000Z
- Description: Go 1.26: Memory Allocation Optimization Analysis. Go 1.26 RC1 has been released, which means the official release is just around the corner. In the Go 1.26。
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

Go 1.26 RC1 has been released, which means the official release is just around the corner. In the Go 1.26 release notes, there is one small line that particularly stands out:

> [*reducing the cost of some small (<512 byte) memory allocations by up to 30%*](https://go.dev/doc/go1.26?ref=huizhou92.com#faster-memory-allocation)*.*

This means that, without changing a single line of code, we can get a noticeable performance boost in specific scenarios.

This article takes a deep dive into how Go 1.26 achieves this memory allocation optimization — from benchmark results and assembly analysis to runtime and compiler implementation details — so you can fully understand this compiler–runtime co-design optimization.

**Test platform**

```shell
goos: darwin 
goarch: arm64 
pkg: blog-example/go1.26/malloc 
cpu: Apple M4 
✗ go version 
go version go1.24.9 darwin/arm64 
✗ gotip version 
go version go1.26-devel_f8ee0f84 Fri Jan 2 19:26:36 2026 -0800 darwin/arm64
```

### Small Object Allocation Benchmarks

To validate this improvement, I wrote a simple benchmark:

```go
// https://gist.github.com/hxzhouh/f662b3b149e10106f6a30a5355883919 
package main   
import (   
    "testing"   
)   
   
// Structs of different sizes   
type Small16 struct {   
    A int64   
    B int64   
}   
   
type Small32 struct {   
    A, B, C, D int64   
}   
   
type Small64 struct {   
    Data [8]int64   
}   
   
// Global sinks to prevent full optimization   
var (   
    sink16 *Small16   
    sink32 *Small32   
    sink64 *Small64   
)   
   
// --- 16-byte allocation --- 
func BenchmarkAlloc16(b *testing.B) {   
    for i := 0; i < b.N; i++ {   
       // In Go 1.26, this call is replaced with a specialized mallocgcSmallNoscan16 
       sink16 = &Small16{A: int64(i), B: int64(i)}   
    }   
}   
   
// --- 32-byte allocation --- 
func BenchmarkAlloc32(b *testing.B) {   
    for i := 0; i < b.N; i++ {   
       sink32 = &Small32{A: 1, B: 2, C: 3, D: 4}   
    }   
}   
   
// --- 64-byte allocation --- 
func BenchmarkAlloc64(b *testing.B) {   
    for i := 0; i < b.N; i++ {   
       sink64 = &Small64{}   
    }   
}   
   
// --- Allocation with pointers (Scan path) --- 
type WithPtr struct {   
    A *int   
    B int   
}   
   
var sinkPtr *WithPtr   
   
func BenchmarkAllocWithPtr(b *testing.B) {   
    val := 42   
    for i := 0; i < b.N; i++ {   
       sinkPtr = &WithPtr{A: &val, B: i}   
    }   
}
```

Run the benchmarks:

```shell
gotip test -bench=. -count=10 > new_results.txt  
go test -bench=. -count=10 > old_results.txt
```

**Benchmark comparison results:**

![Golang 1.26 vs 1.24 Memory Allocation Benchmark](https://cdn-images-1.medium.com/max/800/0*oH5emkP-QRyDubux.png)

benchmark result

- **16-byte objects**: 7.24ns → 5.08ns (**\~30% faster**)
- **32-byte objects**: 9.17ns → 6.03ns (**\~34% faster**)
- **64-byte objects**: 10.08ns → 7.66ns (**\~24% faster**)
- **Objects with pointers**: 9.77ns → 5.77ns (**\~41% faster**)

These numbers clearly demonstrate the significant improvement in small-object allocation in Go 1.26.

Let’s analyze the assembly differences using a simpler example:

```go
type Data32 struct {   
    a, b, c, d int64   
}   
   
//go:noinline   
func createData() *Data32 {   
    // In Go 1.26, this is optimized into a specialized allocator call 
    return &Data32{a: 1, b: 2, c: 3, d: 4}   
}
```

Inspect the assembly:

```shell
go build -gcflags="-S" malloc.go 2>&1 |grep CALL 
gotip build -gcflags="-S" malloc.go 2>&1 |grep CALL
```

![](https://cdn-images-1.medium.com/max/800/0*OLQvjCP0HXNyepCH.png)

In Go 1.24, the code calls the generic `runtime.newobject(SB)`.  
In Go 1.26, it directly calls the specialized `runtime.mallocgcSmallNoScanSC4(SB)`.

This is the root cause of the performance difference.

### The “All-Purpose” Burden of the Generic Allocator

In Go 1.25 and earlier, almost all heap allocations (such as `new(T)`) eventually funnel through a single generic entry point: `runtime.mallocgc`.

This function is potent — but that power comes with overhead. It must handle:

- **All sizes**: from 8 bytes to multiple megabytes
- **All types**: scan vs. noscan
- **All states**: GC marking, GC assist, zeroing, etc.

Because it must be “universal,” `mallocgc` contains complex logic and multiple runtime checks. Even allocating a 16-byte integer requires recomputing the size class, loading type metadata, and executing various branches. This **runtime polymorphism** adds measurable overhead.

In Go 1.26, the compiler no longer blindly calls the generic allocator. Instead, it directly invokes size- and type-specialized allocation functions whenever possible.

### Go 1.26’s Secret Weapon: Specialized Malloc

Go 1.26 introduces **Size-Specialized Malloc**, which fundamentally resolves this structural inefficiency.

Given that:

- Size class boundaries are static
- Small object sizes are known at compile time
- Pointer information is part of the type system

There is no need to recompute these decisions at runtime.

So Go 1.26 makes a critical change:

**It generates a dedicated allocation template for each size class and lets the compiler select it directly during the SSA phase.**

From this point on:

- Size class selection is no longer a runtime responsibility
- `mallocgc` is no longer on the hot path for small objects
- The runtime only executes an already-optimal plan

This is what it means to:

> *Shift allocation decisions left — from runtime to compile time.*

At its core, this is a textbook example of **compiler–runtime co-design optimization**.

**High-level flow:**

![graph TD A[“Go source: &Data32{…}”] → B[“Compiler SSA phase”] B → C{“Size known at compile time?”} C — “YES” → D{“< 512B?”} C — “NO” → E[“runtime.newobject”] D — “YES” → F[“Specialized call: mallocgcSmallNoScanSC4”] D — “NO” → E F → G[“Direct mcache.alloc[4] access”] G → H[“Fast allocation + inline zeroing”]](https://cdn-images-1.medium.com/max/800/1*77k9glp6W_7jJOBritnliQ.png)

### Template System: AST-Level “Surgery”

Rather than using string-based templates like `text/template`, Go 1.26 adopts a much more hardcore approach: **AST (Abstract Syntax Tree) manipulation**.

The core templates live in `src/runtime/malloc_stubs.go`. Functions like `mallocStub` look like ordinary Go code, but they contain special placeholder variables such as `elemsize_`, `sizeclass_`, and `noscanint_`.

```go
// src/runtime/malloc_stubs.go (simplified) 
func mallocStub() unsafe.Pointer { 
    // sizeclass_ will be replaced with a constant (e.g. 4) 
    sizeclass := sizeclass_ 
    // ... 
}
```

This approach has two major advantages:

1. **Type safety**: The template is valid Go code and fully type-checked.
2. **Precision**: The generator operates on syntax nodes, allowing exact replacement of identifiers without the risk of string-based errors.

### Code Generation: The Unsung Hero `mkmalloc`

Located in `src/runtime/_mkmalloc/`, `mkmalloc.go` and `mksizeclasses.go` form the “factory” behind this optimization. They run during toolchain build time and generate the final specialized allocator code.

#### 1\. Precision Size Table Computation (`mksizeclasses.go`)

This step computes the optimal size classes:

- **Fragmentation control**: It simulates layouts to keep tail waste under 12.5%.
- **Division elimination**: It precomputes magic numbers so runtime divisions like `offset / size` become `(offset * magic) >> shift`.

#### 2\. Static Specialization and Forced Inlining (`mkmalloc.go`)

This is where most of the performance comes from.

- **Constant folding**  
 Template variables like `elemsize_` are replaced with literals (e.g. `32`), allowing dead code elimination to remove unnecessary branches.
- **Manual AST inlining**  
 To bypass compiler inlining limits, `mkmalloc` directly injects the AST of helper functions like `nextFreeFast` and `writeHeapBitsSmall` into the generated allocator.  
 The result: zero function calls and a straight-line instruction sequence.
- **Fast dispatch tables**  
`malloc_tables_generated.go` builds arrays like `mallocNoScanTable`, mapping size classes directly to specialized functions.

```go
var mallocNoScanTable = [513]func(...) unsafe.Pointer{ 
    // ... 
    mallocgcSmallNoScanSC4, // index 4 
    // ... 
}
```

### Compiler SSA Optimization

During compilation, SSA performs logic similar to:

```go
func (s *state) expr(n *ir.Node) *ssa.Value { 
    switch n.Op() { 
    case ir.ONEW: 
        typ := n.Type().Elem() 
        if typ.Size() <= 512 && typ.Size() > 0 { 
            sizeClass := sizeToClass(typ.Size()) 
            if !typ.HasPointers() { 
                return s.newValue1(ssa.OpMallocSmallNoScanSC, sizeClass, ...) 
            } 
            // Scan variants handled similarly 
        } 
        return s.newValue0(ssa.OpNewObject, ...) 
    } 
}
```

### Size Class System Explained

Go’s allocator follows the TCMalloc design and uses **size classes** to fight fragmentation.

#### 67 Size Classes

Go 1.26 defines 67 size classes (0–66):

```go
const _NumSizeClasses = 67 
var class_to_size = [_NumSizeClasses]uint16{ 
    0, 
    8, 
    16, 
    24, 
    32, 
    48, 
    // ... 
}
```

#### Allocation Strategy

![](https://cdn-images-1.medium.com/max/800/1*PXOI4Vyu38ZFDfUJUEkdKA.png)

### Why Go 1.26 Is Faster

#### Fewer Indirections

- **Old**: `newobject` → `mallocgc` → checks → size class → span → alloc
- **New**: `mallocgcSmallNoScanSC4` → `mcache.alloc[4]` → alloc

#### Eliminated Type Checks

Specialized allocators hardcode size and pointer properties.

#### Inline Zeroing

With compile-time constants, `memclrNoHeapPointers(x, 32)` becomes a few MOV instructions—no function call.

#### Better CPU Cache Behavior

Direct `mcache.alloc[N]` access improves predictability and cache locality.

### Applicability and Limitations

**Requirements:**

1. Fixed size at compile time
2. Object size < 512 bytes
3. `GOEXPERIMENT=sizespecializedmalloc` enabled (default in 1.26)
4. No sanitizers (race/asan/msan/valgrind), non-Plan9

### Conclusion

Go 1.26’s memory allocation optimization is a **textbook example of compiler–runtime co-design**.

#### Key Takeaways

1. AST-based template generation
2. SSA-based allocation dispatch
3. Manual AST-level inlining
4. Constant folding enabling intrinsic zeroing

### Advice for Developers

This is a **free performance**:

1. Upgrade to Go 1.26
2. No change code
3. Prefer fixed-size small objects in hot paths

While keeping Go simple, the language continues to push performance boundaries through increasingly sophisticated compiler techniques.

### References

- [Go 1.26 Release Notes](https://go.dev/doc/go1.26?ref=huizhou92.com)
- [Go Source: ](https://github.com/golang/go/blob/master/src/runtime/malloc.go?ref=huizhou92.com)[runtime/malloc.go](https://github.com/golang/go/blob/master/src/runtime/malloc.go?ref=huizhou92.com)
- [Change Log: Size-specialized malloc](https://go-review.googlesource.com/c/go/+/665835?ref=huizhou92.com)