Go's GC Shape Stenciling: A Middle Ground for Generics
Go's generics implementation uses GC shape stenciling, a hybrid approach that monomorphizes based on garbage collector shape rather than exact types, balancing binary size and performance.

Go's generics implementation takes a pragmatic middle path between full monomorphization (Rust, C++) and type erasure (Java). The approach, called GC shape stenciling, compiles one function body per GC shape rather than per concrete type argument. This keeps binary sizes manageable while avoiding the runtime overhead of boxing and casts.
How GC Shapes Work
A type's GC shape is determined by its size, alignment, and pointer layout—essentially how the garbage collector sees it. Two types share a GC shape if they have the same underlying type, with one key exception: all pointer types collapse into a single shape named after *uint8. This means *User and *Order share one compiled body, while int and float64 each get their own.
The compiler performs stenciling: it substitutes each distinct GC shape for the type parameter and generates one function version per shape. When the function body needs to know the exact type (e.g., for calling a method or reflection), Go passes a hidden dictionary argument—a read-only table containing runtime type descriptors for each concrete instantiation.
Trade-offs vs. Other Approaches
Full monomorphization (Rust, C++) produces optimal code per type but can bloat binaries. Type erasure (Java) keeps a single body but forces boxing for primitives and runtime casts. GC shape stenciling sits in between: it generates multiple bodies but fewer than full monomorphization, and it avoids runtime type information loss through dictionaries.
For example, calling identity with int, float64, *User, and *Order produces three function bodies (one for int, one for float64, one shared for pointers) and four dictionaries. The pointer types share code, but each instantiation still gets its own dictionary for type-specific operations.
Practical Implications
This design means Go generics incur no runtime overhead for type abstraction—no boxing, no casts—while keeping compile times and binary sizes lower than full monomorphization. The trade-off is that the compiler must manage dictionaries, and generic functions that rely heavily on type-specific behavior may see slightly more indirection. For most real-world code, the approach hits a sweet spot.
Go's generics proposal left the implementation strategy open; GC shape stenciling is what shipped in Go 1.18 and remains the default. It reflects the language's philosophy of pragmatic engineering over theoretical purity.
Discussion
0 Comments
Be the first to start the discussion.