Changed
- Handing an
AsyncMutex,AsyncRWLockorAsyncSemaphorefrom one task
to the next costs less. The wait queue keeps its bookkeeping in the
waiters, in fields the primitive's own lock already guards, and every
access to them was checked again at run time for an overlapping access —
some twenty checks on the way to each handoff; the link from each waiter
back to the one ahead of it was a checkedunownedreference besides,
which updates the waiter's reference counts atomically on every load and
store. Neither is there now. On the package's own measurements a
contended handoff costs about 5 to 10 percent less, and the uncontended
take is unchanged. - Handing an
AsyncMutex,AsyncRWLockorAsyncSemaphorefrom one task
to the next costs less again. The wait queue is generic over what a waiter
asks for, and nothing specialized it across the module boundary, so every
handoff went through the generic form; its entry points now carry
specializations for both of the things a waiter asks for. The table it
keeps of the last waiter at each priority present was an array of pairs
holding a waiter, so every priority read from it was reference counted;
the priorities are walked in an array of their own now. On the package's
own measurements a contended handoff costs about 8 to 12 percent less for
the first and a further 4 to 7 percent for the second, and cancelling a
queuedAsyncSemaphorewait about 11 percent less. The uncontended take
is unchanged. - Releasing an
AsyncMutex, or anAsyncRWLockheld for writing, no longer
copies the record of the holder it gives up, nor does the check each
handoff makes for a holder to escalate. The copy retained and released the
holder's task on the way out; reading the one field wanted in place does
neither. On the package's own measurements an uncontended write to an
AsyncRWLockcosts about 8 to 10 percent less, and an uncontended
AsyncMutexbuilt with Swift 6.3 about 14 percent less. Semaphoreon Apple platforms takes and returns a permit with one atomic
operation each, which cannot fail. It compared and exchanged before, and
with several threads on one semaphore that had permits to spare — a pool,
a limit on concurrency — most attempts failed and were retried, each one
the word's cache line fetched for nothing. On the package's own
measurements an uncontendedwait()andsignal()cost about a quarter
less, asDispatchSemaphore's do; fourteen threads sharing fourteen
permits cost a fifth of what they did. The count's limit on Darwin is
Int32.maxnow, where it wasUInt32.max.- Threads reading one
RWLockno longer start at the same slot of the
table readers publish themselves in, except on Windows. Two that did
handed the slot's cache line back and forth on every read: with twelve
threads that was a pair on about one lock in four, at some ten times the
cost of a read for the two of them. - Taking an
RWLockfor reading costs less. The table readers publish
themselves in was allocated the first time one was needed and reached, on
every read after that, through the accessor a lazily initialized global is
reached by — a call on the read path, and an allocation on whichever read
came first, which a caller that must not allocate could not avoid. The
table is storage in the binary now, left zero-filled by the loader, so
neither happens. On the package's own measurements a read among twelve
threads retires about 29 percent fewer instructions and takes about 6
percent less time, and an uncontended one retires about 5 percent fewer
and takes the same; a read whose section is long enough to dominate is
unchanged. - An
RWLockthat is mostly read costs less on Apple platforms. A writer
waits for the readers that were inside the lock when it arrived, and it
slept for them at once: two system calls, for readers a few instructions
from leaving, with every reader that came after the writer queued behind
it meanwhile. It looks for their departure for a few microseconds first
now, and sleeps only if they are still there. On the package's own
measurements, twelve threads each writing once in a hundred turns cost
about a third less a turn while the section is under a microsecond, and
about 14 percent less at 1.1 µs; an uncontended read or write is
unchanged.
Fixed
- A value copied out of an
RWLockread — a class reference, or a string,
array or other copy-on-write value — could be freed out from under the
reader. On Darwin, musl and Windows, a reader that took the lock through
the table readers publish themselves in released it with an atomic store,
and Swift's reference-counting optimizer moves a retain past any store:
the retain that makes the copy the reader's own ran after the lock was
released, and a writer replacing the value in between freed it first. The
process crashed, or went on with a reference to freed memory. The release
is an atomic exchange now, which the optimizer does not move a retain
past. Every release since 1.0.1, which introduced the table, is affected;
a value read without copying anything out, or of a type holding no
references, never was.
Full Changelog: v1.1.1...v1.1.2