How can a library with library evolution still offer a fast generic function, and what does @_specialize(exported: true) do?
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…