What is swift_getGenericMetadata?

I'm profiling some Swift code, and I'm using about 2.5% of my execution time on swift_getGenericMetadata from libswiftCore.dylib. It's happening inside a symbol named: "type metadata accessor for MyType".

Why is this required? It looks like it's querying type information at runtime, but I would have assumed a generic function would be compiled with all the required type information encoded in it statically.

It's used to fetch metadata for a generic type.

a generic function would be compiled with all the required type information encoded in it statically.

Are you referring to monomorphization? Swift only does that as an optimisation, it's not really a language feature.

Is there a way to ensure monomorphization? Is that something which will happen automatically when compiling with optimization, or is there a compiler setting/ hint to ensure this behavior?

Maybe you can use the @_specialize(...) attribute to force it (and keep in mind that this is an internal attribute whose behaviour can always change)

My advice in the attached thread would apply when you are trying to speed up generic code. Xcode’s Instruments labels specialized methods as such, so it is usually easy to tell how far down the call stack the compiler first failed to specialize something. Ultimately the compiler needs access to the implementation in order to specialize.

The undocumented @_specialize(...) will force specialization, but the compiler still needs to see enough to select the right specialized function, or else it has to sort out that selection at runtime. That causes it to have mixed results, because in some situations it can actually make things slower. I don’t recommend using it unless you really know what you are doing and you have no other choice.

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?

  1. When compiling for size using -Os (probably). Specialization increases binary size as you get multiple implementations of the same function.
  2. When not optimising at all. Specialisation is an optimisation.
  3. 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.

4 Likes

Thank you for the tip about @inlinable. I have exactly the case where one module declares a number of generic functions which are performance sensitive, and used by other modules. I look forward to seeing if I can get a ~2% performance increase from this.

Out of curiosity: does this only work when the source of the module declaring the generic function is available during compilation, or can the inlined version somehow be generated from the .swiftmodule file or something? In other words, if I compiled a standalone dylib and linked to it in a completely separate compilation process, can I still get the inlined version?

So the source of truth for this is @jrose's portion of his and @harlanhaskins great WWDC session Binary Frameworks in Swift, and I'd recommend watching that session.

The short takeaway is that for binary frameworks .swiftmodule isn't what matters, .swiftinterface is. A .swiftinterface file is a textual representation of the API of a Swift module that works across multiple compiler versions.

When something is not marked @inlinable, this module contains only the interface to a function: its arguments and return types, its method labels, etc. When you mark something @inlinable the entire method body is part of the .swiftinterface file.

The result of this is that: yes, if you distribute a binary Swift framework with @inlinable code, you can still get the inlined version.

Please be aware that if you are intending to distribute proper binary frameworks with a stable ABI that you cannot evolve any @inlinable function or @usableFromInline data without risking breaking your ABI. Please use that with caution, and watch the session above for a deeper discussion of these issues.

5 Likes