Repository navigation
Description
Weak maps with primitive value storage still walk every entry during ephemeron value marking, even though their values cannot contain GC references. The collector calls MarkWeakHashValues twice per mark, so this adds two unnecessary complete map scans to each collection.
This is especially visible in minor collections on a stable old map. Reproduced on Windows x64, Haxe 4.3.7, MSVC 19.44.35213, libhxcpp 2573b94, with HXCPP_GC_GENERATIONAL.
The probe retains N key objects in an array and inserts them into either WeakMap<Item,Int> or ObjectMap<Item,Int>, with value 7 for every entry. A temporary population of N*8 objects is created on a joined attached native worker and dropped to provide spare heap capacity. Three setup collections precede the timed section. Keys and maps remain unchanged throughout timing.
Each process requests 41 collections, discards the first ten, and reports the median of the remaining 31. The native mark ID is checked around every call: all 41 collections are minor, with no full collections. The retained keys and selected lookups are checked outside the timer.
| Retained entries | Strong-map control, minor GC | Weak map, minor GC | One direct primitive ephemeron scan |
|---|---|---|---|
| 262144 | 0.1248 ms | 3.0479 ms | 1.0391 ms |
| 524288 | 0.1972 ms | 12.8622 ms | 4.3458 ms |
| 1048576 | 0.3024 ms | 28.0221 ms | 9.0280 ms |
The last column is a separate native probe calling the actual hash object's virtual markWeakValues method twenty times and reporting the average per call. It first verifies that the hash uses hashInt storage, and every call returns false. It does not substitute an optimized implementation. An earlier fresh process at 1048576 entries measured 27.9300 ms for the weak map and 0.3529 ms for the control.
In src/hx/Hash.h, Hash::markWeakValues only rejects non-weak or unmanaged-key maps before walking the buckets. For primitive values, TMarkWeakValue(T &, MarkContext *) always returns false, but the walk still visits the keys and calls IsWeakRefValid. The runtime already knows the actual value storage type, including after a storage conversion.
Add a fast path for value stores that cannot reference managed memory, such as Int, Float and Int64, before the bucket walk. Base the decision on the runtime storage specialization rather than the Haxe generic declaration: a WeakMap<Key,Dynamic> can start with primitive storage and later convert to managed object or String values. Managed values must retain their ephemeron marking behavior.
The two redundant scans account for roughly 18 ms at the largest size in the direct probe. This is an estimate from phase measurements, not a measured result for a modified collector. Weak-key removal in updateAfterGc still has to run, so removing these scans alone will not make the weak-map result equal the strong-map control. Further tracking of young keys could address the remaining scan separately.
A local conventional test/regression/WeakMapValueStorage covers Int, Float and Int64 values, conversion from Int to managed payloads, young String and object values through old live keys, transient young keys, and value-to-key cycles that must be collected. It passes in generational mode and with generational, moving and verifier modes combined. No runtime implementation changes were made.
Related: #305 concerns scanning dirty old strong maps after writes; this map is unchanged. #203 concerns repeated ephemeron dependency passes; these primitive values cannot add a dependency at all. This can be fixed within libhxcpp without compiler or stdlib changes.
Activity
Skip the ephemeron value walk of weak maps with primitive values