From a80490f63f1cabf5aa8135f58ba9fa76d0ccc57e Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Fri, 30 Oct 2026 08:23:50 +0100 Subject: [PATCH 1/2] Don't enter GC free zones in atomic operations 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 --- src/hx/atomic/AtomicInt.cpp | 18 ------------------ src/hx/atomic/AtomicObject.cpp | 14 -------------- 2 files changed, 32 deletions(-) diff --git a/src/hx/atomic/AtomicInt.cpp b/src/hx/atomic/AtomicInt.cpp index 30bde4ecf..d0c9bb48b 100644 --- a/src/hx/atomic/AtomicInt.cpp +++ b/src/hx/atomic/AtomicInt.cpp @@ -14,43 +14,31 @@ hx::atomic::AtomicInt_obj::AtomicInt_obj(int val) : impl(new Impl()) int hx::atomic::AtomicInt_obj::fetchAdd(int val) { - hx::AutoGCFreeZone _; - return impl->atomic.fetch_add(val); } int hx::atomic::AtomicInt_obj::fetchSub(int val) { - hx::AutoGCFreeZone _; - return impl->atomic.fetch_sub(val); } int hx::atomic::AtomicInt_obj::fetchAnd(int val) { - hx::AutoGCFreeZone _; - return impl->atomic.fetch_and(val); } int hx::atomic::AtomicInt_obj::fetchXor(int val) { - hx::AutoGCFreeZone _; - return impl->atomic.fetch_xor(val); } int hx::atomic::AtomicInt_obj::fetchOr(int val) { - hx::AutoGCFreeZone _; - return impl->atomic.fetch_or(val); } int hx::atomic::AtomicInt_obj::store(int val) { - hx::AutoGCFreeZone _; - impl->atomic.store(val); return val; @@ -58,22 +46,16 @@ int hx::atomic::AtomicInt_obj::store(int val) int hx::atomic::AtomicInt_obj::load() { - hx::AutoGCFreeZone _; - return impl->atomic.load(); } int hx::atomic::AtomicInt_obj::exchange(int val) { - hx::AutoGCFreeZone _; - return impl->atomic.exchange(val); } int hx::atomic::AtomicInt_obj::compareExchange(int expected, int replacement) { - hx::AutoGCFreeZone _; - int original{ impl->atomic.load() }; while (original == expected) { diff --git a/src/hx/atomic/AtomicObject.cpp b/src/hx/atomic/AtomicObject.cpp index b2ea5c5ac..91455e378 100644 --- a/src/hx/atomic/AtomicObject.cpp +++ b/src/hx/atomic/AtomicObject.cpp @@ -34,9 +34,7 @@ void hx::atomic::AtomicObject_obj::__Visit(HX_VISIT_PARAMS) Dynamic hx::atomic::AtomicObject_obj::store(Dynamic val) { - hx::EnterGCFreeZone(); impl->atomic.store(val); - hx::ExitGCFreeZone(); HX_OBJ_WB_GET(this, val.mPtr); @@ -45,16 +43,12 @@ Dynamic hx::atomic::AtomicObject_obj::store(Dynamic val) Dynamic hx::atomic::AtomicObject_obj::load() { - hx::AutoGCFreeZone _; - return impl->atomic.load(); } Dynamic hx::atomic::AtomicObject_obj::exchange(Dynamic val) { - hx::EnterGCFreeZone(); Dynamic old{ impl->atomic.exchange(val) }; - hx::ExitGCFreeZone(); HX_OBJ_WB_GET(this, val.mPtr); @@ -68,20 +62,12 @@ Dynamic hx::atomic::AtomicObject_obj::compareExchange(Dynamic expected, Dynamic Dynamic original{ impl->atomic.load() }; while (original == expected) { - hx::EnterGCFreeZone(); if (impl->atomic.compare_exchange_strong(original.mPtr, replacement.mPtr)) { - hx::ExitGCFreeZone(); - HX_OBJ_WB_GET(this, replacement.mPtr); return original; } - else - { - hx::ExitGCFreeZone(); - continue; - } } return original; From 074e52b1959c20da579cedaaaa4a5e80972e1f2d Mon Sep 17 00:00:00 2001 From: Simon Krajewski Date: Fri, 30 Oct 2026 10:54:47 +0100 Subject: [PATCH 2/2] Make atomic operations GC safe points 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 --- src/hx/atomic/AtomicInt.cpp | 21 +++++++++++++++++++++ src/hx/atomic/AtomicObject.cpp | 11 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/hx/atomic/AtomicInt.cpp b/src/hx/atomic/AtomicInt.cpp index d0c9bb48b..0e9575e0f 100644 --- a/src/hx/atomic/AtomicInt.cpp +++ b/src/hx/atomic/AtomicInt.cpp @@ -2,6 +2,9 @@ #include "hx/atomic/AtomicInt.hpp" #include +// Atomic operations never block, so they don't enter a GC free zone. Each one is still a GC +// safe point, so that a loop which spins on atomics without allocating can't stall a collection. + struct hx::atomic::AtomicInt_obj::Impl { std::atomic_int atomic; @@ -14,31 +17,43 @@ hx::atomic::AtomicInt_obj::AtomicInt_obj(int val) : impl(new Impl()) int hx::atomic::AtomicInt_obj::fetchAdd(int val) { + __hxcpp_gc_safe_point(); + return impl->atomic.fetch_add(val); } int hx::atomic::AtomicInt_obj::fetchSub(int val) { + __hxcpp_gc_safe_point(); + return impl->atomic.fetch_sub(val); } int hx::atomic::AtomicInt_obj::fetchAnd(int val) { + __hxcpp_gc_safe_point(); + return impl->atomic.fetch_and(val); } int hx::atomic::AtomicInt_obj::fetchXor(int val) { + __hxcpp_gc_safe_point(); + return impl->atomic.fetch_xor(val); } int hx::atomic::AtomicInt_obj::fetchOr(int val) { + __hxcpp_gc_safe_point(); + return impl->atomic.fetch_or(val); } int hx::atomic::AtomicInt_obj::store(int val) { + __hxcpp_gc_safe_point(); + impl->atomic.store(val); return val; @@ -46,16 +61,22 @@ int hx::atomic::AtomicInt_obj::store(int val) int hx::atomic::AtomicInt_obj::load() { + __hxcpp_gc_safe_point(); + return impl->atomic.load(); } int hx::atomic::AtomicInt_obj::exchange(int val) { + __hxcpp_gc_safe_point(); + return impl->atomic.exchange(val); } int hx::atomic::AtomicInt_obj::compareExchange(int expected, int replacement) { + __hxcpp_gc_safe_point(); + int original{ impl->atomic.load() }; while (original == expected) { diff --git a/src/hx/atomic/AtomicObject.cpp b/src/hx/atomic/AtomicObject.cpp index 91455e378..d7babd200 100644 --- a/src/hx/atomic/AtomicObject.cpp +++ b/src/hx/atomic/AtomicObject.cpp @@ -2,6 +2,9 @@ #include "hx/atomic/AtomicObject.hpp" #include +// Atomic operations never block, so they don't enter a GC free zone. Each one is still a GC +// safe point, so that a loop which spins on atomics without allocating can't stall a collection. + struct hx::atomic::AtomicObject_obj::Impl { std::atomic atomic; @@ -34,6 +37,8 @@ void hx::atomic::AtomicObject_obj::__Visit(HX_VISIT_PARAMS) Dynamic hx::atomic::AtomicObject_obj::store(Dynamic val) { + __hxcpp_gc_safe_point(); + impl->atomic.store(val); HX_OBJ_WB_GET(this, val.mPtr); @@ -43,11 +48,15 @@ Dynamic hx::atomic::AtomicObject_obj::store(Dynamic val) Dynamic hx::atomic::AtomicObject_obj::load() { + __hxcpp_gc_safe_point(); + return impl->atomic.load(); } Dynamic hx::atomic::AtomicObject_obj::exchange(Dynamic val) { + __hxcpp_gc_safe_point(); + Dynamic old{ impl->atomic.exchange(val) }; HX_OBJ_WB_GET(this, val.mPtr); @@ -57,6 +66,8 @@ Dynamic hx::atomic::AtomicObject_obj::exchange(Dynamic val) Dynamic hx::atomic::AtomicObject_obj::compareExchange(Dynamic expected, Dynamic replacement) { + __hxcpp_gc_safe_point(); + // Note: using Dynamic instead of hx::Object* is important // Dynamic has an overloaded == operator, a raw pointer to hx::Object does not. Dynamic original{ impl->atomic.load() };