Use std::atomic, and delete the hand-written atomics - #81
Merged
Merged
Conversation
Atomic.hpp carried a hand-written implementation of atomic load and store
-- compiler barriers via __asm__ __volatile__ ("" ::: "memory"), CPU
fences via __sync_synchronize(), and a CAS loop on
__sync_bool_compare_and_swap. stdinc.h defined
MATHICGB_USE_CUSTOM_ATOMIC_X86_X64 in both the _MSC_VER and __GNUC__
branches, so on x86 and x64 that, and not std::atomic, is what
MonomialMap and FixedSizeMonomialMap have been using.
Its own comment gave the premise: "it is surprising that any compiler
ships with a std::atomic that is worse than this - but that is very much
the case." That was written in 2013 against GCC 4.x and it is no longer
true. Compiling both implementations at -O2 with GCC 13.3 on x86-64
and comparing what comes out:
order custom std::atomic
------------------------------------------------------------------
load relaxed movq movq
load acquire movq movq
load seq_cst movq movq
store relaxed movq movq
store release movq movq
store seq_cst movq; lock cmpxchgq; jne xchgq
Identical for every operation on the hot path, and on the one operation
where they differ the hand-written version is the worse of the two: a
compare-and-swap with a retry branch where std::atomic emits a single
locked exchange. So this is not a wash. It removes a slower
implementation.
That store is used once, at MonomialMap.hpp:188, on the map-growth path.
Everything in the traversal and insertion paths -- relaxed, consume and
release -- compiles to the same instructions either way, which is why
timing cannot see the change: interleaved runs of hyclic8-101-trimmed and
cyclic7 at one and four threads, nine reps each, land between -0.5% and
+4.4% with run-to-run deviation of 3-7%. Where the emitted code is the
same there is nothing to time.
Also gone with it: the AtomicInternal barrier helpers and seqCstStore,
which existed only to serve the custom class, and the class comment's
claim about MSVC 2012, which cannot apply to a project that has required
C++17 since PR #60 and whose VS2012 files were removed in PR #78.
MATHICGB_USE_FAKE_ATOMIC is untouched and still compiles. It selects a
no-synchronization implementation for measuring the overhead of the
atomic operations, which is a separate facility from the one removed
here.
Verified: cmake Release and Debug both pass 246/246, autotools make check
and make distcheck pass, a build with -DMATHICGB_USE_FAKE_ATOMIC still
compiles, and single-threaded output is byte-identical to the old code on
cyclic5, cyclic7, hyclic8-101-trimmed and hilbertkunz1. Multithreaded
output cannot be compared this way: the same binary produces a different
.gb on five consecutive cyclic7 runs, and identical output at
-threadCount 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FakeAtomic is a no-op atomic: load returns the member, store assigns it, with no atomicity, no ordering and no constraint on the compiler. It arrived in c1105ec, on 2012-11-06, in the same commit that added the hand-written atomics, and that commit says what it was for -- to "determine the overhead of the ordering constraints ... IF using the custom Atomic and not std::atomic". It was an instrument for measuring the implementation the previous commit deleted. There is no longer anything for it to measure. Compiling the same translation unit with GCC 13.3 at -O2, the whole difference between FakeAtomic and std::atomic is one instruction -- the sequentially consistent store, movq against xchgq. Every load, and the relaxed and release stores, are byte identical. That store happens once per map growth, at MonomialMap.hpp:188. It is also a trap. Nothing defines the macro: there is no configure option, no cmake option and no CI cell, only a hand-written -D. Build that way and the unit tests pass, 246 of 246, which is worse than failing: the suite runs multithreaded under TBB, so those runs contain real data races that happen not to manifest on x86-64, and a green suite invites the conclusion that the configuration is supported. With FakeAtomic gone, ChooseAtomic has one implementation to choose from, so it goes too and Atomic holds a std::atomic<T> directly. Atomic.hpp is 53 lines, from 398 before this pair of commits. The class still earns its place: it asserts alignment and it withholds operator= and operator T() so that loads and stores have to be written out. A build that still passes -DMATHICGB_USE_FAKE_ATOMIC is not an error; the macro is simply ignored. Verified: cmake Release and Debug both pass 246/246. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the hand-written implementation and the fake-atomic switch gone, Atomic<T> was a wrapper that forwarded load and store to a std::atomic member. Its own comment said it was equivalent to std::atomic<T>, and after those two commits that is nearly true. Nearly, because it differed in two ways. It withheld operator= and operator T(), which std::atomic provides and which do sequentially consistent loads and stores silently -- and a seq_cst store is the one operation with a cost here, xchgq against a plain movq. And its constructor value-initialized, where std::atomic's default constructor is trivial in C++17 and leaves the value indeterminate. The second difference is the one that mattered, and it is the reason FixedSizeMonomialMap nulls its buckets by hand. Removing the wrapper without saying so would leave that nulling looking like belt and braces. Measured, with the nulling taken out of the two constructors: with the wrapper the unit tests still pass 246/246, because its constructor zeroed the array; with plain std::atomic the same build segfaults, and valgrind reports the buckets being walked as a hash chain in F4MatrixBuilder2::findOrCreateColumn under TBB. The first constructor already explained this, and now does so about the code as written rather than by analogy, since the member really is a std::atomic. It is left as it was, except that its claim that new int[x]() "is supposed to zero initialize but this apparently does not work on GCC" is dropped: new std::atomic<void*>[64]() into deliberately dirtied memory comes back fully null on GCC 13.3, and a comment that now has to be relied on should not carry a false aside. Using that value-initializing form here would mean changing make_unique_array, which PolyHashTable and mathicgb.cpp also use, so the explicit nulling stays. The second constructor allocates its buckets the same way and now points at the first. The alignment assertion goes with the wrapper. It was load-bearing when the member was a raw T and atomicity depended on alignment; a std::atomic aligns itself, and alignof(std::atomic<void*>) is 8. Verified: cmake Release and Debug pass 246/246, autotools make distcheck passes, and valgrind reports no errors on an F4 run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The file list still described Atomic.hpp as a MathicGB alternative to std::atomic, necessary "because the std::atomic implementations that shipped with GCC and MSVC were so slow that they were just completely unusable", and documented MATHICGB_USE_FAKE_ATOMIC. None of that survives this branch. The section also posed this: Project (medium-effort, easy-difficulty): Figure out if GCC and MSVC really do ship a usable-speed std::atomic now and, if so, which versions are good and which are bad. Then let Atomic be implemented in terms of std::atomic on those good versions while retaining the fast custom implementation for the bad versions. The first commit on this branch answers the first half for GCC 13.3: usable-speed, and in fact better than the custom implementation on the one operation where the two differ. The second half is moot -- there are no bad versions left to retain a custom implementation for, and it is gone. Elsewhere the description of F4MatrixBuilder2's hash table said it is implemented using "std::atomic (well, actually mgb::Atomic, but it's the same thing)". Now it is std::atomic, so the aside goes and the paragraph is rewrapped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d-torrance
added a commit
that referenced
this pull request
Sep 18, 2026
MATHICGB_STREAM_CHECK asserted and then threw, so in a Debug build the assert fired first and the twenty checks in the public streaming interface aborted where the throw would have reported the problem. What they report is caller error -- calling idealBegin() twice, appending more terms than were promised -- not a bug in the library, so throwing is the whole of the right answer and the assert is simply in the way. Nothing had ever caught this. Every test that drives an ideal stream follows the protocol, so no check had fired, and before PR #80 no CI cell built this code with assertions at all. The new test constructs a StreamStateChecker directly and walks it into three violations, which is the first time any of these twenty checks has been exercised. The message drops "Assert expression:" for "Failed check:", there being no assert left to name. This also clears the 19 clang warnings that PR #81's CI turned up in the macOS debug cells. They came from the assert(("message", condition)) idiom, which puts a comma operator with a no-effect left operand into every expansion; deleting the assert removes the idiom. clang 18.1.3 reports 19 warnings in this file before the change and 0 after, and GCC never warned. One thing deliberately left out: StreamStateChecker's constructor catches its own check without rethrowing, so with the assert gone a composite modulus double-frees instead of aborting. That path is unreachable through the public interface -- the modulus comes from a GroebnerConfiguration that has already rejected a composite one -- and the real fix is to stop owning the Pimpl by raw pointer. Left for its own item, which is why no composite-modulus test appears here. Verified: 251/251 in cmake Release and Debug, no build warnings from GCC 13.3, and the new test passes in both configurations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
src/mathicgb/Atomic.hppimplemented atomic load and store by hand — compilerbarriers via
__asm__ __volatile__ ("" ::: "memory"), CPU fences via__sync_synchronize(), and a CAS loop on__sync_bool_compare_and_swap.stdinc.hdefinedMATHICGB_USE_CUSTOM_ATOMIC_X86_X64in both the_MSC_VERand
__GNUC__branches, so on x86 and x64 — every ordinary Linux build — that,and not
std::atomic, is whatMonomialMapandFixedSizeMonomialMapused.Its own comment gave the premise to test:
Written in 2013 against GCC 4.x. It is no longer true, and the measurement is
not close.
What the compiler actually emits
Both implementations, same translation unit, GCC 13.3 at
-O2, x86-64:std::atomicmovqmovqmovqmovqmovq;lock cmpxchgq;jnexchgqIdentical for every operation on the hot path, and on the one operation where
they differ the hand-written version is the worse of the two: a
compare-and-swap with a retry branch where
std::atomicemits a single lockedexchange. So this is not a wash — it removes a slower implementation.
That seq_cst store is used once, at
MonomialMap.hpp:188, on the map-growthpath. Traversal and insertion are relaxed, consume and release, which compile
the same either way — which is why timing cannot see the change: interleaved
runs of
hyclic8-101-trimmedandcyclic7at one and four threads, nine repseach, land between −0.5% and +4.4% against 3–7% run-to-run deviation. Where the
emitted code is the same there is nothing to time.
The commits
1503063— delete the hand-written implementation. A pure deletion, 329lines, no additions.
ac7225e— removeMATHICGB_USE_FAKE_ATOMIC. It was added inc1105ec(2012-11-06) alongside the custom atomics, to measure their overhead. With
those gone the whole difference between it and
std::atomicis oneinstruction. It is also a quiet trap: nothing defines the macro, and a build
that does passes 246/246 while running the suite multithreaded under TBB with
no synchronization at all.
84989af— removeAtomic.hppitself and usestd::atomicdirectly.3487b2c— updatedoc/description.txt, which described the deletedfile and posed a project this branch answers.
On removing the wrapper, and why the comments changed
After the first two commits
Atomic<T>only forwarded to astd::atomicmember, so the third commit removes it. It differed from
std::atomicin twoways, and one of them is load-bearing:
Atomic()value-initialized, wherestd::atomic's default constructor is trivial in C++17 and leaves the valueindeterminate. That is exactly why
FixedSizeMonomialMapnulls its buckets byhand.
Measured rather than assumed, by deleting the nulling from both constructors:
Atomic<T>std::atomic<T>valgrind on the second:
Conditional jump or move depends on uninitialised value(s)inF4MatrixBuilder2::Builder::findOrCreateColumn, under TBB —garbage bucket pointers walked as a hash chain. So both constructors now say why
the nulling is required. The existing comment is kept as it was, minus one claim
that is no longer true:
new std::atomic<void*>[64]()into deliberately dirtiedmemory comes back fully null on GCC 13.3. Switching to that form would mean
changing
make_unique_array, whichPolyHashTable.cppandmathicgb.cppalsouse, so the explicit nulling stays.
The alignment assertion went with the wrapper. It was load-bearing when the
member was a raw
Tand atomicity depended on alignment; astd::atomicalignsitself, and
alignof(std::atomic<void*>)is 8.Verification
cmake Release and Debug both pass 246/246, autotools
make checkandmake distcheckpass, and valgrind reports zero errors on an F4 run. Single-threadedoutput is byte-identical to the old code on
cyclic5,cyclic7,hyclic8-101-trimmedandhilbertkunz1.Multithreaded output cannot be compared that way, which is worth recording: the
same binary produces a different
.gbon five consecutivecyclic7runs, andidentical output at
-threadCount 1. A first pass at this comparison lookedlike a discrepancy between the two builds and was not one.
This is also the first branch to run under the 16-cell matrix from PR #80, which
is more than routine here:
Atomic.hppcompiled differently on x86 thananywhere else, so the macOS and arm64 cells are the interesting ones.
🤖 Generated with Claude Code