How does an unspecialised generic function know the concrete type, and what does that cost?

5.3.2 · L5 · Learning

Predict first

What hidden parameters does the generic function take in LLVM IR?

func larger<T: Comparable>(_ a: T, _ b: T) -> T { a < b ? b : a }
print(larger(1, 2))
  • None
  • the type metadata and a Comparable witness table
  • a vtable

Mental model. The function gets a toolbox labelled with the type it must work on: how big it is, how to copy it, and which operations it supports.

A generic function’s machine code cannot know the size, copy rules or operations of T. So the caller passes extra hidden arguments: the type metadata for T (a record with its size, alignment and value witness table for copying and destroying), and one protocol witness table for each conformance in the generic constraint. In LLVM IR a function func m<T: Comparable> takes ptr %T and ptr %T.Comparable alongside its normal parameters.

Everything the body does with T goes through them: copying a value calls a value-witness function, comparing calls a witness-table entry, and values of unknown size are passed indirectly in memory instead of in registers. That is the cost specialisation removes. It also explains the ABI of generics across modules, and why some and generic parameters differ from any: the metadata and tables are passed in, rather than packed with the value in an existential box.

The IR signature ends with ptr %T, ptr %T.Comparable: the metadata for the type and its protocol witness table.

Common traps

  • Thinking generics are free everywhere. Unspecialised, each use passes metadata and goes through witness calls.
  • Confusing the generic’s witness tables with an existential’s. A generic passes them separately; an existential stores them with the value.

They might follow up with

  • Where does metadata come from? The compiler emits a metadata record per type, and generic types build theirs lazily at runtime and cache them.
  • What are value witnesses? Function pointers for copy, move and destroy of values whose layout is unknown at compile time.

Related questions

  • 5.3.1 How can you see that a generic function was specialised, and what does it depend on?
  • 5.3.3 What is different in the compiled code between returning some P and any P?
  • 1.4.6 What is an existential container? What is a witness table?

← How can you see that a generic function was specialised, and what…   What is different in the compiled code between returning some P and… →