Add compare-and-swap on foreign memory: %CAS-MEM-UINT32 and -UINT64 - #1841
Open
dg1sbg wants to merge 1 commit into
Open
Add compare-and-swap on foreign memory: %CAS-MEM-UINT32 and -UINT64#1841dg1sbg wants to merge 1 commit into
dg1sbg wants to merge 1 commit into
Conversation
There is no way to atomically swap a word of foreign memory on Clasp. Every route through MP:CAS signals MP:NOT-ATOMIC, because a place is CAS-able only if its accessor carries an ATOMIC-EXPANDER sysprop (lsp/atomics.lisp:8,23) and no FFI accessor has one. The obvious workaround does not exist either: CORE:ACAS is transformed only for element type T and rank 1 (cleavir/primop.lisp:673), so moving a shared counter into a (simple-array (unsigned-byte 32) (*)) does not help. That leaves lock-free code whose state lives in mmap'ed or malloc'ed memory -- shared-memory rings, cross-process counters, allocator metadata -- with no Clasp implementation, where SBCL has (sb-ext:cas (sb-sys:sap-ref-32 sap off) old new). This adds two typed functions rather than an MP:CAS place, which is the cheaper and more honest increment. MP:CAS matches the old value with EQ while CLASP-FFI:%MEM-REF-UINT64 boxes its result, so a value above the fixnum range would compare two distinct bignums and never swap, whereas the hardware compares raw words. A typed function sidesteps boxing entirely and needs no compile-time knowledge of the C type, which the runtime-keyword-dispatching %MEM-REF cannot supply. MP:CAS support for the constant-type case can be layered on top later. The functions return the prior word, so the swap happened iff that value is = to EXPECTED -- the same contract MP:CAS has, and the same as SBCL's. It falls out of __atomic_compare_exchange_n, which overwrites EXPECTED with the word it actually saw. An address not aligned for its width is refused with an error rather than swapped anyway. An unaligned atomic is not lock-free, and silently non-atomic is the exact failure mode of the CORE:ACAS C++ fallback. A static_assert fails the build if either width would need a libatomic lock. Compiler builtins rather than std::atomic_ref because Darwin builds -stdlib=libc++, which only gained atomic_ref in LLVM 19. Six tests: semantics for both widths, four threads times 10000 CAS-increments per width, a neighbouring-word check that catches a wrong swap width, and the unaligned refusal. Measured across processes as well, 4 processes times 50000 increments of one word of POSIX shared memory behind a start barrier: exact with the CAS, 114669 of 200000 with a plain read-modify-write control. Fixes clasp-developers#1835
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.
Fixes #1835.
MP:CASsignalsMP:NOT-ATOMICfor every FFI accessor, and the specialised-arrayworkaround does not exist either (
CORE:ACASis transformed only for element typeTand rank 1,
cleavir/primop.lisp:673), so there is currently no way to compare-and-swapa word of foreign memory on Clasp.
This adds
CLASP-FFI:%CAS-MEM-UINT32andCLASP-FFI:%CAS-MEM-UINT64.Why typed functions rather than an
MP:CASplaceAs noted in the issue,
MP:CASmatches withEQwhile the FFI accessors box theirresults, so a
uint64above the fixnum range would compare two distinct bignums andnever swap; and
%MEM-REFdispatches on a runtime type keyword, while choosing a swapwidth needs the C type at compile time. A typed function sidesteps both.
MP:CASsupportfor the constant-type case can be layered on top later — I am happy to follow up with it
if you would prefer that shape.
Deliberately a
CL_DEFUNrather than a newcc-blir:casproducer: the bytecode VM has noatomic opcode, so a compiler-only route would leave
EVAL, the REPL and every--build-mode=bytecodebuild on a different path.Contract
Returns the prior word, so the swap happened iff that value is
=to the expected one— the same contract
MP:CAShas, and the same as SBCL's(sb-ext:cas (sb-sys:sap-ref-32 sap off) old new). It falls out of__atomic_compare_exchange_n, which overwritesexpectedwith the word it saw.An address that is not aligned for its width is refused with an error rather than swapped
non-atomically, and a
static_assertfails the build if either width would need alibatomic lock. Compiler builtins rather than
std::atomic_refbecause Darwin builds-stdlib=libc++, which only gainedatomic_refin LLVM 19.Tests
Six, in
regression-tests/mp.lisp: semantics for both widths, 4 threads × 10000CAS-increments per width, a neighbouring-word check that catches a wrong swap width, and
the unaligned refusal.
Verified on macOS arm64, boehmprecise, native: full suite 2013 successes against a
2007 baseline taken on this same base immediately before the change — +6 is exactly
the new tests, with the same 5 expected failures and no unexpected failures.
Cross-process check outside the suite, 4 processes × 50000 increments of one word of POSIX
shared memory behind a start barrier: 200000/200000 with the CAS, 114669/200000
with a plain read-modify-write control.