How can a library with library evolution still offer a fast generic function, and what does @_specialize(exported: true) do?

5.11.4 · L8 · Learning

Predict first

Which function gets a pre-specialised exported symbol for Int?

import Swift
public func plainTotal<T: AdditiveArithmetic>(_ xs: [T]) -> T { xs.reduce(.zero, +) }
@_specialize(exported: true, where T == Int)
public func fastTotal<T: AdditiveArithmetic>(_ xs: [T]) -> T { xs.reduce(.zero, +) }
  • fastTotal only
  • plainTotal only
  • both
  • neither

Mental model. A bakery that normally makes one custom loaf per order also keeps a shelf of the most popular size ready-baked. Orders for that size are served from the shelf; unusual ones are still made to order.

In a library built with -enable-library-evolution a generic function that is not @inlinable compiles once, takes the type through metadata and witness tables, and clients cannot specialise it, because the body is not in the interface. Making it @inlinable exposes the body (and locks the implementation as ABI). The underscored @_specialize(exported: true, where T == Int) offers a third path: the library itself emits a specialised copy for Int and exports it as a symbol; callers that use Int bind to that copy, while other types use the generic entry point. The compiled library shows the difference: fastTotal has an extra exported symbol ending in Si_Ts5 (its pre-specialisation for Int), and plainTotal does not. The standard library uses the same mechanism for common element types.

It is a tool for performance-critical, ABI-stable code and comes with caveats. The attribute is underscored, meaning it is not a language-stability promise and its spelling or behaviour could change; each specialisation adds code size; and it only helps callers that name the listed types at compile time. In an app, or a library without library evolution, the optimiser specialises across files by itself (see the specialisation question), so you almost never write it. I checked that the exported symbol is emitted, not the speed-up, which depends on the caller and was not measured.

The attribute emits a symbol ending Si_Ts5.

Common traps

  • Adding it everywhere “for speed”. It grows the binary, and callers may not use the listed types.
  • Relying on it as stable API. It is underscored; treat it as an implementation tool of one library.

They might follow up with

  • When is @inlinable better? When the body is small and you accept making it part of the ABI.
  • How do you check it worked? Look for the specialised symbol in the assembly or with nm.

Related questions

  • 5.3.1 How can you see that a generic function was specialised, and what does it depend on?
  • 5.3.2 How does an unspecialised generic function know the concrete type, and what does that cost?
  • 5.4.1 What does -enable-library-evolution change, and why would you not turn it on for an app?
  • 5.4.2 What is a .swiftinterface file, and what does it show about @inlinable and @frozen?
  • 5.2.3 How does the inliner decide, and what do @inline(never), @inline(__always) and @inlinable do?

← What is strict memory safety (Swift 6.2), what does the unsafe…