Repository navigation
Don't enter GC free zones in atomic operations - #1443
Merged
Merged
Conversation
Operations on std::atomic never block, so they don't need to let a collection run alongside them. Entering and leaving a zone is expensive: on Windows it costs two kernel calls (SetEvent/ResetEvent) and takes the global gThreadStateChangeLock, which made each atomic operation take roughly 650 ns instead of a few. For AtomicObject, the zone was also unsafe: a collection running while the thread is in the zone can move the referenced object, while load() is still handing out the raw pointer it read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tobil4sk
reviewed
Oct 30, 2026
The GC free zones also made every atomic operation a GC safe point. A loop that spins on atomics without allocating relies on that: without it, a collection started by another thread waits for the spinning thread forever. Check for a pending collection at the start of each operation instead, which costs a read of gPauseForCollect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
|
Our combined intelligence isn't quite sure if we should really be doing this in the atomic functions in the first place, but everyone agrees that it's an improvement over the current state, so I'll merge this. |
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.
Since #1441, every
AtomicIntandAtomicObjectoperation enters and leaves a GC free zone. These operations are onstd::atomicand never block, so a free zone isn't needed, and it is costly, mostly on Windows. This PR removes the zones. It keeps the one useful side effect: each operation is still a GC safe point.Cost
On Windows,
HxSemaphoreis a Win32 event. Entering a zone callsSetEvent, and leaving it callsResetEventwhile holding the globalgThreadStateChangeLock. That's two kernel calls and a process-wide lock for every atomic operation.Measured on Windows (MSVC, release), 1,000,000 operations in a loop:
AtomicInt.loadAtomicInt.addAtomicInt.load, 4 other threads allocatingBefore #1441, cpp atomics were inline intrinsics, so this is a regression. In hxcoro, a work-stealing thread pool does a few atomic operations per scheduled event. It slowed down by about 15–25x on Windows, and every thread contended on
gThreadStateChangeLock. With this change, a benchmark round went from 3.6 s to 0.23 s.GC safe points
Entering a free zone also made each atomic operation a GC safe point. A loop that spins on atomics without allocating depends on that (see HaxeFoundation/haxe#13087). Without it, a collection started by another thread waits for the spinning thread forever.
Each operation now starts with
__hxcpp_gc_safe_point(), which only readsgPauseForCollectunless a collection is pending. In a test where a thread spins oncompareExchangeandloadwhile another allocates enough to trigger collections, removing the zones alone hung in the first round, and with the safe points all 200 rounds passed.Correctness for
AtomicObjectA collection can run while a thread is inside a GC free zone, and the GC can move objects.
__Visitstores the moved pointer back for this reason.load()read the raw pointer inside the zone and converted it toDynamicbefore leaving the zone, so a collection in that window could leave it holding a stale pointer. Without the zone, no collection can run during the operation.Relation to #1442
#1442 fixes
AtomicInt.compareExchangereporting success for exchanges that didn't happen. Faster atomics make that race much easier to hit. With this PR alone, the hxcoro thread pool benchmark crashed in most runs. With #1442 as well, 16 of 16 runs passed. I'd merge #1442 first, or both together.🤖 Generated with Claude Code