Add the net9 Interlocked byte and short overloads - #607
Merged
Conversation
Eight members: Exchange and CompareExchange over byte, sbyte, short and ushort. The implementation choice was the whole question, and it was settled by measurement rather than reasoning. Widening to a CompareExchange on the containing 32 bit word is the usual trick, but it writes the neighbouring bytes back, so a concurrent plain write to an adjacent byte can be lost. The BCL demonstrably does not do that: 33 million iterations of a plain write to buffer[1] racing Interlocked.Exchange on buffer[0] produced zero lost writes on net11, so it uses a real byte width atomic. Widening would have been a silent downgrade, not a slowdown. It also needs the address to find the aligned word, so it can touch memory outside the allocation. A lock is used instead. It keeps each operation indivisible against every other one, and since these members do not exist at all below net9 there is nothing outside this file to interoperate with. Writing only the target byte leaves neighbours alone, which is asserted by a test that reproduces the adjacency experiment and passes on the polyfill as well as the BCL. The one real divergence, that a concurrent plain write to the same location can be lost where the BCL operation is indivisible, is noted on all eight. API count 1162 -> 1170.
This was referenced Sep 10, 2026
This was referenced Sep 11, 2026
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.
Eight members, all net9:
ExchangeandCompareExchangeoverbyte,sbyte,shortandushort. The shortlist said "Interlocked.Exchange", singular; the release added the narrow-integer family as a set, andCompareExchangeis the more useful half.The generic
And<T>/Or<T>(net11) were already polyfilled, so this closes the type apart from thenuintoverloads.The implementation choice, settled by measurement
There is no 8- or 16-bit interlocked primitive below net9, so something has to give. The textbook trick is to widen: find the containing aligned 32-bit word and run a
CompareExchangeloop on it. I did not use it, for two reasons — and the first one only became clear by testing the BCL rather than reasoning about it.Widening writes the neighbouring bytes back. A CAS on the containing word rewrites all four bytes, so a concurrent plain write to an adjacent byte can be lost. That would only be acceptable if the BCL behaved the same way. It does not: on net11, 33 million iterations of a plain write to
buffer[1]racingInterlocked.Exchange(ref buffer[0], …)produced zero lost writes, so the runtime is using a genuine byte-width atomic instruction. Widening would therefore have been a silent correctness downgrade, not merely a slowdown — exactly the failure mode that shows up under load and nowhere else.It also needs the address. Locating the aligned word requires pointer arithmetic, which can read and write outside the allocation for a location at the end of one, and would restrict the whole thing to consumers who set
AllowUnsafeBlocks— leaving the API present for some consumers and absent for others.So: a lock
It keeps each operation indivisible with respect to every other one, and — the point that makes this defensible — these members do not exist at all below net9, so every byte or short interlocked operation on those targets necessarily goes through this file. There is nothing outside it to interoperate with.
It also writes only the target byte, so neighbours are untouched, matching the BCL. That is not asserted by inspection:
DoesNotDisturbTheAdjacentBytereproduces the adjacency experiment as a test and passes on the polyfill as well as on the BCL.The one real divergence is that a concurrent plain write to the same location can be lost, where the BCL's operation is indivisible. A plain write racing an interlocked operation is already a data race, but it is a genuine difference and is
//Note:d on all eight members, along with the lock.Tests
Four tests on every target framework, so net9.0 and later check the BCL and everything below checks the polyfill:
CompareExchangereturns the original whether or not the comparand matched, and the sign-boundary cases (sbyte.MinValue,ushort.MaxValue)CompareExchangeincrement loop over aushort, asserting that no update is lost — the contract these members actually have to keepVerification
Solution clean in Release, Consume clean across all 22 TFMs, tests green on net11.0 (1749), net10.0 (1749), net9.0 (1749), net8.0 (1746), net462 (1695), plus PublicTests, EmbeddedTests, UnsafeTests, NoRefsTests and NoExtrasTests.
API count 1162 → 1170.