I want to call out these two features, because when combined with optimisation levels they more or less entirely determine whether the generic function is specialised.
(Warning: I don't know the actual heuristics for when the compiler specialises generics, I can only state what I've observed from the outside.)
In my observation, when compiling with -O
, the compiler will specialise any generic function for the arguments with which it is called if it knows their concrete types. Consider this code:
/// Sometimes you just really like Python
func len<T: Collection>(_ t: T) -> Int {
return t.count
}
By default, if this was your whole program, Swift would not specialise this function: it would only emit the generic implementation.
If your program gets slightly longer:
/// Sometimes you just really like Python
func len<T: Collection>(_ t: T) -> Int {
return t.count
}
func main() {
print("The length of [1, 2, 3] is \(len([1, 2, 3]))")
}
main()
Now the behaviour of the Swift compiler changes. In addition to emitting the generic version, Swift will also emit a specialised version, specialised for Array<Int>
. You can observe this in the code: as @SDGGiesbrecht mentioned, the symbol names will tell you when a function is a "generic specialisation".
When won't the compiler specialise?
- When compiling for size using
-Os
(probably). Specialization increases binary size as you get multiple implementations of the same function.
- When not optimising at all. Specialisation is an optimisation.
- When there is an optimisation boundary in the way. The most common boundary is a module boundary. Swift can do "whole-module" optimisation, but not "cross-module" optimisation.
Conveniently, there is a way to fix (3) if you don't care about ABI stability, which is to mark your generic functions @inlinable
. SwiftNIO does this extensively on any part of our public API that is generic. If the function is @inlinable
then the implementation of the function becomes visible to the compiler when it is compiling the module using the code, and that implementation is required in order to specialise the function. The result will be that the calling module will gain a specialised version of the function in its code, while the declaring module will only contain the generic function (and any specialisations that it uses itself).
I would strongly recommend against @_specialize
. We have never gotten it to behave in a way we were really happy with in SwiftNIO. It works, but there are strange wrinkles around visibility that make it hard for callers to actually use the forcibly-specialised version. I recommend simply using @inlinable
where it is appropriate, and trusting the compiler to sort everything else out.