Go

How does the Go compiler implement generics (GC shape stenciling with dictionaries)? What are the performance implications?

Question 130HardGo 1.22 to 1.25

Go uses a hybrid of the two classic strategies. Full monomorphization (C++, Rust) makes one copy per type. Full boxing (Java) makes one copy for everything. Go instead produces one instantiation per GC shape. The GC shape is determined by the underlying type and memory layout. All pointer types share one shape. int and type MyInt int also share a shape. Each call receives a hidden dictionary that carries type descriptors, method addresses and sub-dictionaries for the concrete type arguments.

type Shape interface{ Area() float64 }

// When T is *Circle or *Square (same pointer shape) this body is shared,
// and x.Area() goes through the dictionary: an indirect call, no inlining.
func TotalArea[T Shape](xs []T) (s float64) {
    for _, x := range xs {
        s += x.Area()
    }
    return
}

// With T=int / T=float64 each gets its own shape and is effectively
// monomorphized: operators compile to direct machine instructions.
func SumN[T int | float64](xs []T) (s T) {
    for _, x := range xs {
        s += x
    }
    return
}

Implications:

  • Generics are about as fast as hand-written code for value-type containers and operators.
  • Calling methods through a type parameter instantiated with pointers or interfaces can be slower than a plain interface. The call goes through a dictionary lookup and then possibly an itab, and it blocks inlining and escape analysis.
  • Binary size grows less than with C++-style templates.

Advice: benchmark hot paths, and do not reach for generics just to "avoid interface overhead". PGO (Go 1.21+) can devirtualize hot interface calls, but it does little for dictionary-based calls inside shared generic bodies.

More on Generics

All 36 Generics questions