Repository navigation
Description
The collector frees every buffer left over in largeObjectRecycle at the start of each large-allocation sweep, while all Haxe threads are stopped. After a burst, a collection with almost no live graph work can spend tens of milliseconds returning already-dead cached buffers to the native allocator.
Reproduced on Windows x64, Haxe 4.3.7, MSVC 19.44.35213, default GC, libhxcpp 920fa85.
The probe keeps one 256 MiB Bytes buffer alive to provide a stable live heap and enough recycle allowance. Each round:
- Force a full collection to clear the previous cache.
- On a joined native worker attached through
hx::NativeAttach, allocate N separate 4096-byte Bytes buffers, touch them, and drop them when the worker exits. Automatic GC is disabled only during this setup so the burst reaches the collection intact. - Re-enable GC and run
Gc.run(false)once. This discovers the garbage and puts its large allocations into the recycle cache. - Time the next
Gc.run(false), which purges that cache. Immediately time another collection as the empty-cache control. - Check that the retained buffer is intact.
Nine rounds, discard the first two, median of seven. These measurements use an ordinary build without collector profiling:
| Dead payload in the cache | Number of 4096-byte buffers | Purging collection | Following empty-cache collection |
|---|---|---|---|
| 32 MiB | 8192 | 2.9521 ms | 0.0756 ms |
| 128 MiB | 32768 | 12.9377 ms | 0.0879 ms |
| 256 MiB | 65536 | 28.4493 ms | 0.1056 ms |
A second process at 65536 buffers measured 26.1782 ms versus 0.1027 ms. The temporary array's backing allocation also enters the cache, so the dead payload column counts the byte buffers rather than every cached byte.
A separate build with HXCPP_GC_PROFILE_COLLECT confirms where the time goes: the final seven purge collections have median total 25.98 ms, with 25.89 ms in the large sweep. Each reports large(2->2, recyc 65537). The live large-allocation count is unchanged, and the phase is dominated by releasing cached garbage rather than scanning a large live list.
In src/hx/gc/Immix.cpp, Collect loops over the old largeObjectRecycle entries and calls HxFree on every one before resetting its size and examining mLargeList. Mutators are resumed much later. This is separate from allocation-time cache lookup (#300), large-list removal (#298), and sweeping old live allocations during minor collections (#318).
Consider detaching the expired recycle batch while stopped, then releasing it after mutators resume, or using bounded background cleanup. Keep ownership unambiguous, account for pending native memory, prevent reuse after a buffer is queued for release, and bound retained memory and cleanup backlog. Explicit compaction must retain a useful way to request memory release. Merely keeping every expired batch would trade the pause for uncontrolled memory retention.
A local conventional test/regression/LargeRecyclePurgeLifecycle covers partial cache reuse followed by cache purges, mixed-size allocation churn, zero initialization, and independent retained buffer contents. It passes with default and moving GC. This is correctness coverage for a future change; the timing probe is separate and the regression has no timing assertion. No runtime changes were made. The work can be done entirely in libhxcpp.
Activity
Free a dropped large-buffer cache after the collection