From b5025f4ef153d899cd56853692ea80cc082f0989 Mon Sep 17 00:00:00 2001 From: Todd White Date: Tue, 11 Aug 2026 17:23:04 -0400 Subject: [PATCH 1/2] Take the association lock only where a reference is installed setReference searched the reference list for the key, then took the striped lock around a block whose only statement was a test for that search having failed. Replacing the value of a key already present therefore acquired and released the lock around nothing: the value and the policy are written after that block, outside the lock, guarded separately and only when either policy is atomic. The search now decides whether the lock is taken at all, and a second search runs under it, so two threads that both find the key absent no longer install two references for one key. One association per thread on distinct objects, 32 cores, ns per objc_setAssociatedObject: 14.1 to 4.4 at 1 thread, 66.5 to 4.5 at 4, 112.6 to 4.7 at 8, 224.0 to 9.2 at 16 and 461.0 to 12.3 at 24. objc_getAssociatedObject is unchanged at 2.7. The suite passes, 198 of 198. The unsynchronized read of a key in findReference is unchanged; it is what the search outside the lock already did. --- associate.mm | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/associate.mm b/associate.mm index 9cee7b8f..3093eef8 100644 --- a/associate.mm +++ b/associate.mm @@ -135,12 +135,14 @@ static void setReference(struct reference_list *list, case OBJC_ASSOCIATION_ASSIGN: break; } - // While inserting into the list, we need to lock it temporarily. + // While inserting into the list, we need to lock it temporarily. An + // existing reference is updated in place. struct reference *r = findReference(list, key); + if (NULL == r) { auto lock = acquire_locks_for_pointers(list); - // If there's an existing reference, then we can update it, otherwise we - // have to install a new one + // Another thread may have installed this key since the search above. + r = findReference(list, key); if (NULL == r) { // Search for an unused slot From a4c054e90779f860116facfb0d6a916977cfe06a Mon Sep 17 00:00:00 2001 From: Todd White Date: Wed, 12 Aug 2026 19:36:42 -0400 Subject: [PATCH 2/2] Take a per-object lock when installing an associated reference lock_for_pointer discards the low 8 bits of the pointer, so two reference lists within 256 bytes of each other take the same lock whatever the size of the striped table. The reference list now carries a ThinLock of its own, used only for the structure of the list. Installing a reference on distinct objects, one per thread, ns per objc_setAssociatedObject: 14.4 to 14.6 at 1 thread, 1857.8 to 31.9 at 8, 7313.0 to 53.7 at 24. A striped table of 65536 entries instead reaches 2059.5 at 24 threads. ctest passes 198 of 198. --- associate.mm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/associate.mm b/associate.mm index 3093eef8..8a3e4e55 100644 --- a/associate.mm +++ b/associate.mm @@ -54,6 +54,11 @@ * @syncronize(). */ mutex_t lock; + /** + * Lock guarding the structure of the list. Only the first reference list + * in a chain uses it. + */ + ThinLock structureLock; /** * Array of references. */ @@ -140,7 +145,7 @@ static void setReference(struct reference_list *list, struct reference *r = findReference(list, key); if (NULL == r) { - auto lock = acquire_locks_for_pointers(list); + std::lock_guard lock{list->structureLock}; // Another thread may have installed this key since the search above. r = findReference(list, key); if (NULL == r)