How can you see that a generic function was specialised, and what does it depend on?

5.3.1 · L5 · Interview

Predict first

Is there a specialised copy of twice in the SIL of a Debug (-Onone) build?

@inline(never) func twice<T: Numeric>(_ x: T) -> T { x + x }
print(twice(2))
  • Yes
  • No

Predict first

And with -O?

@inline(never) func twice<T: Numeric>(_ x: T) -> T { x + x }
print(twice(2))
  • Yes
  • No

Mental model. A generic function is a recipe written for “any grain”. Specialising makes a laminated card that says “rice”, with all the grain-detection steps removed.

A generic function can be compiled once, working on any type through hidden metadata, or specialised: the optimiser copies the body for a known type (say Int), replacing every witness-table call with direct code, which can then be inlined and folded. Specialised functions are visible in SIL by their mangled names, which gain a Tg5 suffix (“generic specialisation”). In -Onone there are none: Debug builds run the generic code as written.

Specialisation needs two things: the optimiser must be on, and it must see the generic’s body at the call site that fixes the type. Within one WMO module that is automatic. Across a module boundary it is only possible if the function is @inlinable (or marked @_specialize(where T == Int) to emit a pre-specialised copy). A generic that is passed around as an existential, or whose concrete type is only known at runtime, stays unspecialised. That is why “generic code is as fast as hand-written code” is true in your module and often false across a binary-library boundary.

Specialisation is an optimiser pass, so an unoptimised build keeps the single generic version.

The optimiser sees the call twice(2), knows T == Int, and emits a specialised copy marked Tg5.

Common traps

  • Benchmarking generics in a Debug build. Nothing is specialised, so the numbers say little about Release.
  • Putting a hot generic in a separate library without @inlinable. Clients get the slow, unspecialised version.

They might follow up with

  • What does Tg5 mean? It is part of Swift’s symbol mangling for a generic specialisation. Demangle the full name to see the type.
  • Can the compiler specialise through a protocol existential? Sometimes: it can open the existential and specialise on the concrete type when it can prove it.

Related questions

  • 5.3.2 How does an unspecialised generic function know the concrete type, and what does that cost?
  • 5.2.3 How does the inliner decide, and what do @inline(never), @inline(__always) and @inlinable do?
  • 1.4.7 What is generic specialization? When does it not happen?

How does an unspecialised generic function know the concrete type… →