Skip to content

Let selector table readers share the lock - #414

Draft
DTW-Thalion wants to merge 1 commit into
gnustep:masterfrom
DTW-Thalion:perf/selector-table-rwlock
Draft

Let selector table readers share the lock#414
DTW-Thalion wants to merge 1 commit into
gnustep:masterfrom
DTW-Thalion:perf/selector-table-rwlock

Conversation

@DTW-Thalion

Copy link
Copy Markdown
Contributor

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.

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
DTW-Thalion marked this pull request as draft August 13, 2026 11:59
Comment thread lock.h
# 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants