Skip to content

Avoid scanning every old finalizer during minor collections #302

Closed

Description

@dimensionscape

Every minor collection walks all registered finalizers, including finalizers belonging to old objects that the collection cannot reclaim. A large retained population can make a nursery collection take tens of milliseconds even when no callback runs.

In src/hx/gc/Immix.cpp, RunFinalizers scans sgFinalizers, sFinalizableList, sFinalizerMap and sHaxeFinalizerMap. The two maps walk every unordered_map node and fetch the object's mark byte. RunFinalizers runs after both full and minor marking. The old-object mark check succeeds, but the complete scan has already happened.

Measured on Windows x64, MSVC 19.44.35213, Haxe 4.3.7, libhxcpp d734565, release build with HXCPP_GC_GENERATIONAL:

Retained objects Registered Haxe finalizers, median minor GC Same objects after unregistering finalizers
16,384 0.1498 ms 0.0569 ms
100,000 0.8057 ms 0.0603 ms
500,000 26.0621 ms 0.0871 ms
1,000,000 66.1647 ms 0.1289 ms

These are focused collection measurements, not application speedup estimates. The callback throws if invoked, and no callback ran. The same array and every Item remain strongly reachable in both phases. Objects are created before measurement. Temporary garbage is created on a joined native thread to leave enough free heap for nursery mode. GC stays enabled throughout. Each phase performs three setup collections followed by 71 timed collections, discards the first ten samples and reports the median of the remaining 61. hx::gByteMarkID was unchanged across every timed collection, confirming that the measurements were minor collections.

A separate one-million-object run registered, unregistered, then registered the same finalizers again: 71.1413 ms, 0.1295 ms and 61.1033 ms. Re-registering restores the cost without changing the retained object population.

A possible runtime-only improvement is to partition finalizer registrations by collection generation, processing only nursery candidates on minor collections and all candidates on full collections. Registration on an already old object, promotion, callback replacement/removal, moving GC and reentrant callbacks all need explicit handling. Any partition must preserve exactly-once cleanup and must not turn the registry into a strong owner of the finalized object. The native and internal finalizer paths need evaluation too; the timing above exercises the Haxe finalizer map only.

Local conventional coverage was added at test/regression/GenerationalFinalizerLifecycle/{Main.hx,build.hxml,stdout.txt}. It passes on the current runtime and checks that retained old objects are not finalized, dead young objects are finalized exactly once, explicit unregistering is respected, and later collections do not repeat callbacks. It has no timing assertions. Add benchmark coverage for increasing old finalizer populations, both native and Haxe registrations, and small nursery populations before changing the bookkeeping.

Related: #186 removes unnecessary registrations for trivial native boxes. This issue concerns the recurring cost of valid registrations that must remain. 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. Each of the four finalizer tables now has an old part, for entries whose object survived a collection: such an object can only die in a full collection, so a minor collection looks at the new entries alone and moves the survivors across, and a full collection looks at both. Registering again, replacing or removing a finalizer takes it out of either part. Moving, visiting and pinning handle both parts. With 500,000 finalized objects a median minor collection went from 25.6 ms to 0.085 ms, the same as with none (Windows x64, HXCPP_GC_GENERATIONAL). Test: GenerationalFinalizerLifecycle.

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