Skip to content

Avoid rescanning stable old weak references during every minor GC #314

Closed

Description

@dimensionscape

Every minor collection scans all registered native weak references, including old references to live old targets. Large weak-reference caches therefore add recurring nursery-collection latency even when their target population is unchanged and cannot be reclaimed by that collection.

Measured on Windows x64, MSVC 19.44.35213, Haxe 4.3.7 and libhxcpp fe67557, release HXCPP_M64 with HXCPP_GC_GENERATIONAL:

Retained weak-reference wrappers Empty-reference control, median minor GC References to retained old targets, median minor GC
100,000 0.1263 ms 0.2899 ms
1,000,000 0.7123 ms 3.3421 ms
2,000,000 1.4532 ms 6.8315 ms

Each case runs in a fresh process. Both controls allocate and retain the same number of Item targets, Haxe WeakRef wrappers and native weak-reference objects. The difference is whether the native weak references point to those retained targets or to null. The null-reference objects do not enter the global processing list. The targets remain strongly reachable in both cases, so none of the registered references should clear.

Setup creates temporary garbage on a joined native worker to leave sufficient free heap for nursery mode. GC stays enabled. Each process performs three setup collections, then 71 timed Gc.run(false) calls, discarding ten warm-up samples and reporting the median of 61. hx::gByteMarkID is unchanged across every timed call, confirming minor collections; all cases recorded zero full collections. Target/ref value checks happen outside the timer. These are focused GC measurements, not an application-wide speedup estimate.

In src/hx/gc/Immix.cpp, WeakRef registers each non-null target in sWeakRefs. RunFinalizers walks the entire list after marking, checking each reference's mark byte and calling IsWeakRefValid on its target. This processing runs after both minor and full collection. The native WeakRef target does not have a normal mutation API; Haxe WeakRef.set creates a new native weak-reference object and updates its wrapper, rather than replacing the old native object's target in place.

Consider separating nursery weak-reference candidates from old references whose targets are old, so a minor collection need not revisit every stable old entry. Full collection still needs complete liveness processing. Promotion, moving GC, dead reference objects, bound-method target handling and replacement through WeakRef.set all need coverage. Do not make the target strongly owned or delay clearing a dead young target to avoid the scan. Measure the bookkeeping cost for small populations and frequent set/replacement workloads as well as large stable caches.

Conventional local coverage was added first at test/regression/GenerationalWeakRefLifecycle/{Main.hx,build.hxml,stdout.txt}. It passes on the current runtime. It retains old targets, replaces some old wrappers with weak references to young targets, checks held/dead young targets after collection, then releases the old targets and confirms full collection clears them. Construction, checks and releases run on joined workers so stale main-thread stack values do not decide target liveness. The test has no timing assertions.

This concerns the optional generational collector and can be addressed inside libhxcpp without compiler or standard library changes. Related: #302 concerns finalizer registrations, which use different processing tables; #202 concerns callable-adapter weak-reference correctness rather than this scan cost. No runtime implementation change has been made.

Activity

  1. added 2 commits that reference this issue on Oct 25, 2026
    2c6c3d2
    426663c
  2. dimensionscape commented on Oct 25, 2026

    @dimensionscape
    MemberAuthor

    Fixed in 426663cc9851a81ddfe8c6f4e196ac4beb0ac8e1, with #302. Weak references are split the same way: one whose reference and target both survived a collection is left alone by minor collections and checked by every full one. Nothing about clearing changes: a dead young target is cleared by the minor collection that finds it dead. With 1,000,000 weak references to retained targets a median minor collection went from 2.16 ms to 0.29 ms, the same as with null references. Test: GenerationalWeakRefLifecycle. Its last check, that full collections clear released targets, is held on Windows: the Windows CI runners keep one released target alive, on main before this change too, which is #367.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions