Let selector table readers share the lock - #414
Draft
DTW-Thalion wants to merge 1 commit into
Draft
Conversation
The selector table is read far more often than it is written: every sel_getName, and every sel_registerName on a name that is already known, reads it and changes nothing. Both took the same exclusive lock as registration, so readers serialised against each other. The lock becomes a read-write lock. On Windows that is a slim reader/writer lock in place of the critical section, whose exclusive case is a mutex; elsewhere it is a pthreads read-write lock. Slim locks need Windows Vista. objc_register_selector_copy read the table through sel_getName while holding the lock, which worked only because the lock was recursive. It reads through sel_getNameNonUnique instead, which takes no lock and is what register_selector_locked already uses for the same purpose. Both answer the name held by the type list, so the value is unchanged. Test/TypedSelectorRegistration.m registers a typed selector whose untyped form is already known, which is the path that reads the table under the registration lock. It times out against the recursive call and passes without it. Windows, 24 threads, ns per call: sel_registerName on a known name 23871 to 2986, sel_getName 17273 to 2173. Eight threads gain between 1.5 and 3.3 times, and one thread is unchanged. The critical section is what collapses; acquiring and releasing one 24 threads at once costs 17093 ns against 2184 for the shared case of a slim lock. On glibc it is close to a wash: sel_registerName at 24 threads goes from 2654 to 2073 ns, because the lock is held there across a hash lookup long enough for readers to overlap, while sel_getName holds it across a single vector index and does not improve. A pthreads read acquire costs what the mutex cost, both being one atomic read-modify-write on one shared line.
DTW-Thalion
marked this pull request as draft
August 13, 2026 11:59
| # define DESTROY_LOCK(x) DeleteCriticalSection(x) | ||
| // A slim reader/writer lock needs Windows Vista or later. Its exclusive mode | ||
| // is a mutex, so a writer sees the same behaviour as a critical section. | ||
| typedef SRWLOCK rwlock_t; |
Member
There was a problem hiding this comment.
If you're adding this, it's worth doing an audit of the rest of the uses of the LOCK macros. We currently use a recursive mutex for everything, but I think there are actually only two places that should need one:
- The runtime lock (which shouldn't, but I might not have fixed all of the places where it did)
- The locks for
@synchronized, which the language specifies to be recursive.
Recursive mutexes add some overhead vs non-recursive ones (especially on Windows: SRW locks are much faster than critical sections in the uncontended case).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The selector table is read far more often than it is written. sel_getName, and sel_registerName on a name already known, read it and change nothing, but both took the same exclusive lock as registration.
The lock becomes a read-write lock: a slim reader/writer lock on Windows in place of the critical section, a pthreads read-write lock elsewhere. objc_register_selector_copy read the table through sel_getName while holding the lock, which worked only because the lock was recursive. It reads through sel_getNameNonUnique, which takes no lock and is what register_selector_locked already uses for the same purpose.
Slim locks need Windows Vista. On glibc this is close to a wash, a read acquire costing what the mutex cost, so the Windows figures are what carry it.
Windows at 24 threads, ns per call: sel_registerName on a known name 23871 to 2986, sel_getName 17273 to 2173. One thread is unchanged.
Test/TypedSelectorRegistration.m registers a typed selector whose untyped form is already known, the path that reads the table under the registration lock. Its four cases time out against the recursive call and pass without it. 202 tests pass on Linux, 108 on Windows.