Understanding code that leads to swift::_checkGenericRequirements calls

A performance assessment has led me to look into code paths that lead to swift_conformsToProtocol. One of the common callers is swift::_checkGenericRequirements. From a previous forum post ([SE-0143] Dynamic casting for conditional conformances), @Douglas_Gregor describes how conditional conformance leads to a runtime check of generic requirements.

In our code, a primary source of calls to swift::_checkGenericRequirements is coming from RxSwift. However these calls don't seem to be due to conditional conformance. I've reduced some RxSwift code into the below code. This code results in a call to _checkGenericRequirements when the Sink is constructed, and it's not clear to me why a runtime check conformance is needed.

I'd like to learn which patterns of code lead to runtime conformance checks, and ideally how to write code that avoids it. Any directions for further reading/viewing on this subject are much appreciated.

In this code, the Sink<T: ObserverType> class by itself does not trigger _checkGenericRequirements, it's also the presence of let disposable: Disposable. Removing either the protocol constraint, or removing the existential property, will prevent/bypass the generic requirements check at runtime.

protocol ObserverType {}
protocol Disposable {}

class Sink<T: ObserverType> {
    let disposable: Disposable

    init(disposable: Disposable) {
        self.disposable = disposable
    }
}

class Observer: ObserverType {}
class Disposer: Disposable {}
_ = Sink<Observer>(disposable: Disposer())

The stack trace that leads to the swift::_checkGenericRequirements call is:

frame #0:  swift_conformsToProtocol
frame #1:  swift::_conformsToProtocol(swift::OpaqueValue const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptorRef<swift::InProcess>, swift::TargetWitnessTable<swift::InProcess> const**)
frame #2:  swift::_checkGenericRequirements(llvm::ArrayRef<swift::TargetGenericRequirementDescriptor<swift::InProcess> >, llvm::SmallVectorImpl<void const*>&, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>)
frame #3:  _gatherGenericParameters(swift::TargetContextDescriptor<swift::InProcess> const*, llvm::ArrayRef<swift::TargetMetadata<swift::InProcess> const*>, swift::TargetMetadata<swift::InProcess> const*, llvm::SmallVectorImpl<unsigned int>&, llvm::SmallVectorImpl<void const*>&, swift::Demangle::Demangler&)
frame #4:  (anonymous namespace)::DecodedMetadataBuilder::createBoundGenericType(swift::TargetContextDescriptor<swift::InProcess> const*, llvm::ArrayRef<swift::TargetMetadata<swift::InProcess> const*>, swift::TargetMetadata<swift::InProcess> const*) const
frame #5:  swift::Demangle::TypeDecoder<(anonymous namespace)::DecodedMetadataBuilder>::decodeMangledType(swift::Demangle::Node*)
frame #6:  swift_getTypeByMangledNodeImpl(swift::MetadataRequest, swift::Demangle::Demangler&, swift::Demangle::Node*, void const* const*, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>)
frame #7:  swift::swift_getTypeByMangledNode(swift::MetadataRequest, swift::Demangle::Demangler&, swift::Demangle::Node*, void const* const*, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>)
frame #8:  swift_getTypeByMangledNameImpl(swift::MetadataRequest, llvm::StringRef, void const* const*, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>)
frame #9:  swift::swift_getTypeByMangledName(swift::MetadataRequest, llvm::StringRef, void const* const*, std::__1::function<swift::TargetMetadata<swift::InProcess> const* (unsigned int, unsigned int)>, std::__1::function<swift::TargetWitnessTable<swift::InProcess> const* (swift::TargetMetadata<swift::InProcess> const*, unsigned int)>)
frame #10: swift_getTypeByMangledNameInContext
frame #11: ConformsToProtocol`__swift_instantiateConcreteTypeFromMangledName
frame #12: ConformsToProtocol`main [inlined] generic specialization <ConformsToProtocol.Observer> of ConformsToProtocol.Sink.__allocating_init(disposable: ConformsToProtocol.Disposable) -> ConformsToProtocol.Sink<A> at main.swift:0 [opt]

The compiler generates a mangled name from which to generate the metadata for Sink<Observer>, and the runtime demangler calls _checkGenericRequirements to fill in the protocol requirements for Sink when it creates the metadata. This happens only the first time Sink<Observer> is referenced.

Thanks Joe. In some cases _checkGenericRequirements isn't called, and I'm hoping to learn how to reason about when the call happens, and when it doesn't.

For example, by changing the disposable property from type Disposable (a protocol) to Disposer (a class), results in no call to _checkGenericRequirements. In this sample code, Sink needs a constrained generic type and an existential property to trigger a call to _checkGenericRequirements. If either of those are removed, then the call does not happen. What triggers the call, and what doesn't?

First time per Observer, including the product of its associated types? In other words, if there's an Observer whose Element is Int, and another whose Element is String, will these each result in a call to _checkGenericRequirements?

It's possible that there's some optimization that eliminates the class instance entirely that fires when the property is of class type but not when it's an existential for some reason, since your constructor call could get inlined, and the object is never used after it's constructed. If we never construct the object, then we never need the class's metadata. A more reliable way to force the class metadata to be instantiated might be to print it:

print("\(Sink<Observer>.self)")

We'll generate a mangled name for every fully concrete compound type whose metadata we need to instantiate, so Sink<Int> and Sink<String> would both be instantiated once separately. In unspecialized generics, we would not go through the mangler.

Why are you concerned about _checkGenericRequirements calls?

A performance analysis has shown the aggregate of swift_conformsToProtocol are one cause of slow app launching. From Instruments, a majority of these calls are coming from _checkGenericRequirements – which is being triggered by RxSwift.

thanks for this tip

I'm working on making it so that mangled names used for type references directly encode the conformances used by their protocol requirements, which should eliminate the overhead you're seeing. @nate_chandler has also been working on metadata prespecialization for generic types we know are needed at compile time, which could get the runtime out of the business of creating these metadata records entirely. In the meantime, the top-of-tree toolchain also has a frontend flag -Xfrontend -disable-concrete-type-metadata-mangled-name-accessors which will disable the use of the demangler for type metadata accessors and bring you back to the Swift 5.1 behavior. This could have a significant code size impact, though.

Terms of Service

Privacy Policy

Cookie Policy