Skip to content

Track dirty pointer-array ranges to reduce minor GC work after sparse writes #301

Closed

Description

@dimensionscape

With HXCPP_GC_GENERATIONAL, storing one young object into an old pointer array makes the next minor collection rescan the whole array. The write barrier records the array object rather than the changed slots. Large, mostly stable reference arrays therefore add collection work proportional to their total length after a single mutation.

This is an improvement opportunity in the optional generational collector, not a claim that the default collector has this minor-collection behavior.

Measurements

Tested at d734565 on Windows x64, Haxe 4.3.7, MSVC 19.44.35213, release HXCPP_M64 plus HXCPP_GC_GENERATIONAL.

The probe creates an array of distinct Item objects and promotes it. Temporary objects are allocated on a joined/detached worker before measurement, leaving enough free heap for generational collection. Each sample creates one young Item outside timing and keeps it live across the collection in both cases. The control stores an old Item in slot 0; the dirty case stores that young Item in slot 0. No other slot changes.

Timing covers Gc.run(false). Each stage has three non-compacting setup collections, then 71 samples with the first ten discarded. The table is the median of the remaining 61 samples. The exported gByteMarkID is checked before and after every collection: it remained unchanged in all 71 samples of each stage, confirming minor collections rather than full collections. Values are verified after each sample, outside timing. GC remains enabled.

Array entries Old reference written One young reference written
16,384 0.0519 ms 0.0557 ms
262,144 0.0719 ms 0.0925 ms
1,048,576 0.1325 ms 0.3176 ms
4,194,304 0.3644 ms 2.5248 ms

At four million entries, changing one slot adds about 2.16 ms to the minor collection and makes that pass about 6.9 times slower. These compare dirty and clean cases under the current runtime, not an implemented replacement or application-wide speedup.

Runtime opportunity

include/hx/GC.h defines HX_ARRAY_WB(array,index,value) as HX_OBJ_WB(array,value), discarding the index. Array setters also use object-level barriers. Once remembered, Array_obj::__Mark in include/Array.h calls HX_MARK_MEMBER_ARRAY for the complete length. The collector has no array card or dirty-range information to restrict the search.

Investigate card or range tracking for old pointer arrays so sparse writes do not force complete scans. Keep a full-scan fallback for bulk operations and stores that cannot identify a precise range. Existing runtime setters already receive indices, so the work can stay in libhxcpp without compiler or stdlib changes.

Cover typed and dynamic pointer arrays, String arrays, bulk blit/splice/concat, resize/reallocation, boundary slots, moving collection and multiple writers. Audit every use of the array barrier before making its currently ignored index meaningful. Measure mutator overhead and metadata memory as well as collection time; small arrays should retain a cheap path.

Conventional local coverage was added first in test/regression/GenerationalArrayEdges, with Main.hx, build.hxml and stdout.txt. Its build enables HXCPP_GC_GENERATIONAL. It repeatedly replaces the first, middle and last elements of an old array, checks that young references survive collection, preserves unchanged neighbors and verifies a final major collection. It passes on the current runtime and contains no timing assertions.

No runtime implementation changes have been made. Related: #201 reduced remembered-set chunk copying; this concerns how much of a remembered array is scanned afterward.

Activity

  1. added 2 commits that reference this issue on Oct 25, 2026
    1b8593d
    c8469af
  2. dimensionscape commented on Oct 25, 2026

    @dimensionscape
    MemberAuthor

    Fixed in c8469af14f0674b7c40665f0e95741d3fdd04c63. For an array of 1,024 elements or more, the element-store barrier now logs a young value instead of remembering the whole array, and the next minor collection marks the logged values as roots; a full collection drops the log. That keeps a value alive wherever a sort, splice or blit moves it, which a range of dirty slots would not, since those move elements without a barrier. A value overwritten before the collection lives one collection longer. Past a thirty-second of the array's length in a cycle, the array is remembered whole, as before, and every other barrier still remembers the whole array. After one young store in an old 4,194,304-element array a median minor collection went from 3.0 ms to 0.11 ms, and a loop of young stores spread over an old million-element array takes the same time as before. Tests: GenerationalArrayEdges, and GenerationalArrayYoungLog, which moves logged values, overwrites them, stores strings and stores from two threads, and fails under HXCPP_GC_VERIFY if the log is not marked.

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