Skip to content

Commit 7989dbe

Browse files
committed
Protocol conformance cache for generic types
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 #82889 The implementation is controlled by the `SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR` environment variable, which is enabled by default.
1 parent 9ae27d7 commit 7989dbe

8 files changed

+536
-116
lines changed

include/swift/Runtime/Concurrent.h

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,51 @@ struct ConcurrentReadableHashMap {
593593
}
594594
}
595595

596+
// Common implementation for `getOrInsert` and `GetOrInsertManyScope`
597+
template <class KeyTy, typename Call>
598+
void getOrInsertExternallyLocked(KeyTy key, const Call &call) {
599+
auto indices = IndexStorage{Indices.load(std::memory_order_relaxed)};
600+
auto indicesCapacityLog2 = indices.getCapacityLog2();
601+
auto elementCount = ElementCount.load(std::memory_order_relaxed);
602+
auto *elements = Elements.load(std::memory_order_relaxed);
603+
auto *elementsPtr = elements ? elements->data() : nullptr;
604+
605+
606+
auto found = this->find(key, indices, elementCount, elementsPtr);
607+
if (found.first) {
608+
call(found.first, false);
609+
return;
610+
}
611+
612+
auto indicesCapacity = 1UL << indicesCapacityLog2;
613+
614+
// The number of slots in use is elementCount + 1, since the capacity also
615+
// takes a slot.
616+
auto emptyCount = indicesCapacity - (elementCount + 1);
617+
auto proportion = indicesCapacity / emptyCount;
618+
if (proportion >= ResizeProportion) {
619+
indices = resize(indices, indicesCapacityLog2, elementsPtr);
620+
found = find(key, indices, elementCount, elementsPtr);
621+
assert(!found.first && "Shouldn't suddenly find the key after rehashing");
622+
}
623+
624+
if (!elements || elementCount >= elements->Capacity) {
625+
elements = resize(elements, elementCount);
626+
}
627+
auto *element = &elements->data()[elementCount];
628+
629+
// Order matters: fill out the element, then update the count,
630+
// then update the index.
631+
bool keep = call(element, true);
632+
if (keep) {
633+
assert(hash_value(key) == hash_value(*element) &&
634+
"Element must have the same hash code as its key.");
635+
ElementCount.store(elementCount + 1, std::memory_order_release);
636+
indices.storeIndexAt(&Indices, elementCount + 1, found.second,
637+
std::memory_order_release);
638+
}
639+
}
640+
596641
public:
597642
// Implicitly trivial constructor/destructor.
598643
ConcurrentReadableHashMap() = default;
@@ -684,6 +729,48 @@ struct ConcurrentReadableHashMap {
684729
return Snapshot(this, indices, elementsPtr, elementCount);
685730
}
686731

732+
/// A wrapper that allows performing several `getOrInsert` operations under
733+
/// the same lock.
734+
class GetOrInsertManyScope {
735+
GetOrInsertManyScope() = delete;
736+
GetOrInsertManyScope(const GetOrInsertManyScope &) = delete;
737+
GetOrInsertManyScope &operator=(const GetOrInsertManyScope &) = delete;
738+
GetOrInsertManyScope(GetOrInsertManyScope &&) = delete;
739+
GetOrInsertManyScope &operator=(GetOrInsertManyScope &&) = delete;
740+
741+
ConcurrentReadableHashMap &Map;
742+
743+
public:
744+
GetOrInsertManyScope(ConcurrentReadableHashMap &map) : Map(map) {
745+
Map.WriterLock.lock();
746+
}
747+
748+
~GetOrInsertManyScope() {
749+
Map.deallocateFreeListIfSafe();
750+
Map.WriterLock.unlock();
751+
}
752+
753+
/// Get an element by key, or insert a new element for that key if one is
754+
/// not already present. Invoke `call` with the pointer to the element.
755+
///
756+
/// `call` is passed the following parameters:
757+
/// - `element`: the pointer to the element corresponding to `key`
758+
/// - `created`: true if the element is newly created, false if it already
759+
/// exists
760+
/// `call` returns a `bool`. When `created` is `true`, the return values
761+
/// mean:
762+
/// - `true` the new entry is to be kept
763+
/// - `false` indicates that the new entry is discarded
764+
/// If the new entry is kept, then the new element MUST be initialized, and
765+
/// have a hash value that matches the hash value of `key`.
766+
///
767+
/// The return value is ignored when `created` is `false`.
768+
template <class KeyTy, typename Call>
769+
void getOrInsert(KeyTy key, const Call &call) {
770+
Map.getOrInsertExternallyLocked(key, call);
771+
}
772+
};
773+
687774
/// Get an element by key, or insert a new element for that key if one is not
688775
/// already present. Invoke `call` with the pointer to the element. BEWARE:
689776
/// `call` is invoked with the internal writer lock held, keep work to a
@@ -703,48 +790,7 @@ struct ConcurrentReadableHashMap {
703790
template <class KeyTy, typename Call>
704791
void getOrInsert(KeyTy key, const Call &call) {
705792
typename MutexTy::ScopedLock guard(WriterLock);
706-
707-
auto indices = IndexStorage{Indices.load(std::memory_order_relaxed)};
708-
auto indicesCapacityLog2 = indices.getCapacityLog2();
709-
auto elementCount = ElementCount.load(std::memory_order_relaxed);
710-
auto *elements = Elements.load(std::memory_order_relaxed);
711-
auto *elementsPtr = elements ? elements->data() : nullptr;
712-
713-
auto found = this->find(key, indices, elementCount, elementsPtr);
714-
if (found.first) {
715-
call(found.first, false);
716-
deallocateFreeListIfSafe();
717-
return;
718-
}
719-
720-
auto indicesCapacity = 1UL << indicesCapacityLog2;
721-
722-
// The number of slots in use is elementCount + 1, since the capacity also
723-
// takes a slot.
724-
auto emptyCount = indicesCapacity - (elementCount + 1);
725-
auto proportion = indicesCapacity / emptyCount;
726-
if (proportion >= ResizeProportion) {
727-
indices = resize(indices, indicesCapacityLog2, elementsPtr);
728-
found = find(key, indices, elementCount, elementsPtr);
729-
assert(!found.first && "Shouldn't suddenly find the key after rehashing");
730-
}
731-
732-
if (!elements || elementCount >= elements->Capacity) {
733-
elements = resize(elements, elementCount);
734-
}
735-
auto *element = &elements->data()[elementCount];
736-
737-
// Order matters: fill out the element, then update the count,
738-
// then update the index.
739-
bool keep = call(element, true);
740-
if (keep) {
741-
assert(hash_value(key) == hash_value(*element) &&
742-
"Element must have the same hash code as its key.");
743-
ElementCount.store(elementCount + 1, std::memory_order_release);
744-
indices.storeIndexAt(&Indices, elementCount + 1, found.second,
745-
std::memory_order_release);
746-
}
747-
793+
getOrInsertExternallyLocked(key, call);
748794
deallocateFreeListIfSafe();
749795
}
750796

stdlib/public/runtime/EnvironmentVariables.def

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ VARIABLE(SWIFT_DEBUG_ENABLE_SHARED_CACHE_PROTOCOL_CONFORMANCES, bool, true,
6262

6363
#endif
6464

65+
VARIABLE(SWIFT_DEBUG_ENABLE_CACHE_PROTOCOL_CONFORMANCES_BY_TYPE_DESCRIPTOR,
66+
bool, true,
67+
"Enable caching protocol conformances by type descriptor in addition "
68+
"to the cache by metadata.")
69+
6570
VARIABLE(SWIFT_DEBUG_CONCURRENCY_ENABLE_COOPERATIVE_QUEUES, bool, true,
6671
"Enable dispatch cooperative queues in the global executor.")
6772

@@ -70,6 +75,10 @@ VARIABLE(SWIFT_DEBUG_CONCURRENCY_ENABLE_COOPERATIVE_QUEUES, bool, true,
7075
VARIABLE(SWIFT_DEBUG_RUNTIME_EXCLUSIVITY_LOGGING, bool, false,
7176
"Enable the an asserts runtime to emit logging as it works.")
7277

78+
VARIABLE(SWIFT_DEBUG_ENABLE_PROTOCOL_CONFORMANCES_LOOKUP_LOG,
79+
bool, false,
80+
"Enable logs for the conformance lookup routine.")
81+
7382
#endif
7483

7584
VARIABLE(SWIFT_BINARY_COMPATIBILITY_VERSION, uint32_t, 0,

0 commit comments

Comments
 (0)