Skip to content

Don't enter GC free zones in atomic operations - #1443

Merged
Simn merged 3 commits into
masterfrom
remove-atomic-gc-free-zones
Oct 30, 2026
Merged

Simn merged 3 commits into
masterfrom
remove-atomic-gc-free-zones

Conversation

@Simn

@Simn Simn commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Since #1441, every AtomicInt and AtomicObject operation enters and leaves a GC free zone. These operations are on std::atomic and 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, HxSemaphore is a Win32 event. Entering a zone calls SetEvent, and leaving it calls ResetEvent while holding the global gThreadStateChangeLock. 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:

before after
AtomicInt.load ~670 ns 2–3 ns
AtomicInt.add ~660 ns 8–9 ns
AtomicInt.load, 4 other threads allocating ~1050 ns 2 ns

Before #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 reads gPauseForCollect unless a collection is pending. In a test where a thread spins on compareExchange and load while 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 AtomicObject

A collection can run while a thread is inside a GC free zone, and the GC can move objects. __Visit stores the moved pointer back for this reason. load() read the raw pointer inside the zone and converted it to Dynamic before 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.compareExchange reporting 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

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>
Comment thread src/hx/atomic/AtomicObject.cpp Resolved
Simn and others added 2 commits October 30, 2026 10:54
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>
@Simn

Simn commented Oct 30, 2026

Copy link
Copy Markdown
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.

@Simn
Simn merged commit 24d5416 into master Oct 30, 2026
63 checks passed
@Simn
Simn deleted the remove-atomic-gc-free-zones branch October 30, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants