Skip to content

Conversation

DmT021
Copy link
Contributor

@DmT021 DmT021 commented Aug 28, 2025

This change adds a new type of cache (cache by type descriptor) to the protocol conformance lookup system. This optimization is beneficial for generic types, where the same conformance can be reused across different instantiations of the generic type.

Key changes:

  • Add a GetOrInsertManyScope class to ConcurrentReadableHashMap for performing multiple insertions under a single lock
  • Add type descriptor-based caching for protocol conformances
  • Add environment variables for controlling and debugging the conformance cache
  • Add tests to verify the behavior of the conformance cache
  • Fix for Runtime protocol conformance check unexpected first success #82889

The implementation is controlled by the SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR environment variable, which is enabled by default.

This reapplies #82818 after it's been reverted in #83770.

@DmT021
Copy link
Contributor Author

DmT021 commented Aug 28, 2025

@swift-ci please test

@DmT021
Copy link
Contributor Author

DmT021 commented Aug 28, 2025

@tbkka @mikeash Re-applying this change because it was reverted due to some issue, and I don't know what it was. Can you review it again?

@DmT021
Copy link
Contributor Author

DmT021 commented Aug 29, 2025

@swift-ci please test macOS

@mikeash
Copy link
Contributor

mikeash commented Aug 29, 2025

After poking around a bit, the issue is that this change doesn't correctly handle the case when SWIFT_STDLIB_USE_RELATIVE_PROTOCOL_WITNESS_TABLES is on. That results in a crash here:

    if (auto typeDescriptor = type->getTypeContextDescriptor();
        envAllowCacheByDescriptors && allowSaveDescriptor &&
        typeDescriptor && result.witnessTable &&
        CanCacheTypeByDescriptor(*typeDescriptor)) {
      auto conformance = result.witnessTable->getDescription();

In that case, if the table is a relative table, this will read it incorrectly and crash.

I'm working out the best way to test and fix this case. I'll give an update when I have it.

@mikeash
Copy link
Contributor

mikeash commented Aug 29, 2025

That was fun. Testing the SWIFT_STDLIB_USE_RELATIVE_PROTOCOL_WITNESS_TABLES is a bit unpolished.

The fix is to replace auto conformance = result.witnessTable->getDescription(); with:

#if SWIFT_STDLIB_USE_RELATIVE_PROTOCOL_WITNESS_TABLES
      auto conformance = lookThroughOptionalConditionalWitnessTable(
                         reinterpret_cast<const RelativeWitnessTable *>(result.witnessTable))
                         ->getDescription();
#else
      auto conformance = result.witnessTable->getDescription();
#endif

That will handle both cases correctly.

As far as testing goes, I built the freestanding stdlib by using --preset=stdlib_S_standalone_minimal_macho_arm64_relative_protocol_witness_table,build,test with build-script. This failed when building the tests, it did successfully build the runtime library. Then I built a small test program, and compiled it with:

xcrun ../build/Ninja-RelWithDebInfoAssert+stdlib-DebugAssert/swift-macosx-arm64/bin/swiftc -Xcc -ffreestanding -wmo -Xfrontend -enable-relative-protocol-witness-tables -Xlinker ../build/stdlib_S_standalone/swift-macosx-arm64/lib/swift/freestanding/libswiftCore.a proto.swift -Onone -g

The resulting binary is a regular macOS program, but with a statically linked "freestanding" libswiftCore that uses relative witness tables.

This change adds a new type of cache (cache by type descriptor) to the protocol conformance lookup system. This optimization is beneficial for generic types, where the
same conformance can be reused across different instantiations of the generic type.

Key changes:
- Add a `GetOrInsertManyScope` class to `ConcurrentReadableHashMap` for performing
  multiple insertions under a single lock
- Add type descriptor-based caching for protocol conformances
- Add environment variables for controlling and debugging the conformance cache
- Add tests to verify the behavior of the conformance cache
- Fix for swiftlang#82889

The implementation is controlled by the `SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR`
environment variable, which is enabled by default.

This reapplies swiftlang#82818 after it's been reverted in swiftlang#83770.
@DmT021 DmT021 force-pushed the wp/conformance-cache-by-descriptor-2 branch from 6ccc69e to 091b005 Compare August 29, 2025 22:16
@DmT021
Copy link
Contributor Author

DmT021 commented Aug 29, 2025

@swift-ci Please test

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 2, 2025

@swift-ci Please smoke test macOS

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 20, 2025

@swift-ci Please test

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 23, 2025

@mikeash Can we merge this again? Or you want me to add a test for when we have SWIFT_STDLIB_USE_RELATIVE_PROTOCOL_WITNESS_TABLES enabled? I don't really know how to do that, though, so I'd appreciate guidance here.

@mikeash
Copy link
Contributor

mikeash commented Sep 23, 2025

Honestly I'm not sure myself. Manual testing would be OK for now. Do you want to try that? Otherwise I'm happy to repeat my manual testing above with your final revision here.

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 23, 2025

I'd try, yes, but I'm not sure how to invoke build-script with the preset you mentioned.
When I run it with just the preset option, I get
[./utils/build-script] ERROR: no value found for toolchain_path in "%(toolchain_path)s"
Seems like I should provide toolchain_path=<path> as well, but I'm not sure what path I should specify.
Is it the one from Xcode, like /Applications/Xcode26.0.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain or should I build another toolchain from the sources first?

Never mind, I think I figured it out.

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 23, 2025

@mikeash I was able to reproduce the crash on the previous commit before the fix, and verified that it no longer occurs after the fix.
But I encountered a new crash, which is reproducible on main, even without my patch. This crashes with EXC_BAD_ACCESS

protocol Proto {}
class BaseClass: Proto {}
assert(BaseClass.self as Any is Proto.Type)
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x2)
  * frame #0: 0x00000001000ad338 proto_class`swift::ContextDescriptorFlags::isGeneric() const + 12
    frame #1: 0x00000001000abf30 proto_class`swift::TargetContextDescriptor<swift::InProcess>::isGeneric() const + 24
    frame #2: 0x00000001000ece88 proto_class`swift::TargetClassMetadata<swift::InProcess, swift::TargetAnyClassMetadata<swift::InProcess>>::isCanonicalStaticallySpecializedGenericMetadata() const + 44
    frame #3: 0x00000001000ecd30 proto_class`swift::TargetMetadata<swift::InProcess>::isCanonicalStaticallySpecializedGenericMetadata() const + 132
    frame #4: 0x00000001000efb18 proto_class`swift::MetadataResponse performOnMetadataCache<swift::MetadataResponse, swift_checkMetadataState::CheckStateCallbacks>(swift::TargetMetadata<swift::InProcess> const*, swift_checkMetadataState::CheckStateCallbacks&&) + 32
    frame #5: 0x00000001000ef724 proto_class`swift_checkMetadataState + 40
    frame #6: 0x00000001001a1e0c proto_class`tryGetCompleteMetadataNonblocking(swift::TargetMetadata<swift::InProcess> const*) + 48
    frame #7: 0x00000001001a1cb4 proto_class`getSuperclassForMaybeIncompleteMetadata(swift::TargetMetadata<swift::InProcess> const*, std::__1::optional<swift::MetadataState>, bool) + 108
    frame #8: 0x00000001001ac0e8 proto_class`MaybeIncompleteSuperclassIterator::operator++() + 56
    frame #9: 0x00000001001ad180 proto_class`searchInConformanceCache(swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptor<swift::InProcess> const*, bool) + 212
    frame #10: 0x00000001001ac954 proto_class`swift_conformsToProtocolMaybeInstantiateSuperclasses(swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptor<swift::InProcess> const*, bool) + 120
    frame #11: 0x00000001001ac7d8 proto_class`swift_conformsToProtocolWithExecutionContextImpl(swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptor<swift::InProcess> const*, swift::ConformanceExecutionContext*) + 52
    frame #12: 0x00000001001b1714 proto_class`swift_conformsToProtocolWithExecutionContext + 40
    frame #13: 0x00000001000b2bac proto_class`swift::_conformsToProtocol(swift::OpaqueValue const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptorRef<swift::InProcess>, swift::TargetWitnessTable<swift::InProcess> const**, swift::ConformanceExecutionContext*) + 76
    frame #14: 0x00000001000b2d3c proto_class`swift::_conformsToProtocolInContext(swift::OpaqueValue const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolDescriptorRef<swift::InProcess>, swift::TargetWitnessTable<swift::InProcess> const**, bool) + 72
    frame #15: 0x00000001000c3e94 proto_class`_conformsToProtocols(swift::OpaqueValue const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetExistentialTypeMetadata<swift::InProcess> const*, swift::TargetWitnessTable<swift::InProcess> const**, bool) (.171) + 232
    frame #16: 0x00000001000c3ca0 proto_class`_dynamicCastMetatypeToExistentialMetatype(swift::OpaqueValue*, swift::TargetExistentialMetatypeMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool, bool) + 144
    frame #17: 0x00000001000c3b84 proto_class`tryCastToExistentialMetatype(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool, bool) + 284
    frame #18: 0x00000001000bf0a4 proto_class`tryCast(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool, bool) + 272
    frame #19: 0x00000001000bfe18 proto_class`tryCastUnwrappingExistentialSource(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool, bool) + 448
    frame #20: 0x00000001000bf2c4 proto_class`tryCast(swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*&, swift::TargetMetadata<swift::InProcess> const*&, bool, bool, bool) + 816
    frame #21: 0x00000001000beeb8 proto_class`swift_dynamicCastImpl(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, swift::DynamicCastFlags) + 152
    frame #22: 0x00000001000bee10 proto_class`swift_dynamicCast + 56
    frame #23: 0x0000000100000c40 proto_class`main at proto_class.swift:3:1
    frame #24: 0x0000000196396b98 dyld`start + 6076

It looks like the Description field of TargetClassMetadata (read by getDescription()) is 2. I'm not sure what to make of that. Any ideas?
I'm building it as you suggested:

xcrun ../build/Ninja+cmark-DebugAssert+llvm-RelWithDebInfo+swift-DebugAssert+stdlib-RelWithDebInfoAssert/swift-macosx-arm64/bin/swiftc -Xcc -ffreestanding -wmo -Xfrontend -enable-relative-protocol-witness-tables -Xlinker ../build/stdlib_S_standalone/swift-macosx-arm64/lib/swift/freestanding/libswiftCore.a proto_class.swift -Onone -g

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 23, 2025

Oh, it's probably the mismatch for the objc interop support between the compiler and the runtime built by the stdlib_S_standalone_minimal_macho_arm64_relative_protocol_witness_table,build,test preset.
Passing -Xfrontend -disable-objc-interop fixes it.
And in build-presets.ini:

[preset: mixin_stdlib_minimal]
...
swift-objc-interop=0

@mikeash
Copy link
Contributor

mikeash commented Sep 23, 2025

Oh yes, that would do it. The class metadata layout is different with ObjC interop on or off. Sounds like this is all good to go, then.

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 23, 2025

Yeah. I checked again on test/Runtime/protocol-conformance-cache.swift test with -disable-objc-interop and it passes successfully. So I think it's ok now.

@DmT021
Copy link
Contributor Author

DmT021 commented Sep 24, 2025

@mikeash Can you merge please?

@mikeash mikeash merged commit 05dcc6b into swiftlang:main Sep 24, 2025
5 checks passed
@mikeash
Copy link
Contributor

mikeash commented Sep 24, 2025

Off we go! Thanks for working through the second round. Hopefully there is no third. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants