Two load-independent counters from the I6 cc arm say what a RegExp
construction still costs after #9845 (header in the nursery), #9819 (flags
without allocation), #9891 (the write-barrier gate) and #9892 (the literal-site
key). One 3300-character reply, perrymaster, quiet box,
/root/rig9831/regexdiag_I6_3300.txt:
new=1003761 side_table_inserts=2007520 header_bytes=72270792
1. Two address-keyed side-table inserts per construction — 2.0 M per reply
side_table_inserts / new = 2.00 exactly. Every js_regexp_new does:
REGEX_POINTERS.with(|s| s.borrow_mut().insert(ptr)) — a PtrHashSet
insert;
REGEX_SOURCE_TABLE.with(|t| t.borrow_mut().insert(ptr, (pattern, flags))) —
a PtrHashMap insert carrying two Arc<str>.
Each is a PtrHasher hash plus a hashbrown insert, and each is mirrored by a
removal at death and a rekey per evacuation (regex_header_moved_for_gc,
regex_header_clear_dead_for_gc). Both tables are also walked in full after
every copied minor by finalize_dead_copied_minor_from_space_regexps, whose
cost is documented as O(live headers + headers allocated since the last minor)
— and this workload allocates a million of them per reply.
Profile corroboration, from the segment-loop probe (region B, 60,000 reps,
sample, main thread, parser self-check leaf sum == thread header exactly):
|
samples |
% of main thread |
hashbrown PtrHasher inserts under js_regexp_new |
175 |
1.20 % |
regex_header_clear_dead_for_gc (the two removals + expando clear) |
384 |
2.63 % |
(That capture is on a pre-#9845 binary, so the removals sat in the malloc sweep;
on main the same work is in the copied-minor finaliser. The inserts are
unchanged either way.)
The design lead, and it is a subtraction rather than a speed-up.
REGEX_SOURCE_TABLE exists (#637) because the header's pattern_ptr /
flags_ptr were raw pointers into strings the GC could free — the header was
gc_malloc'd and untraced. Since #9845 the header is an ordinary GC object
whose two string slots are traced (GcLayoutSlotKind::RegExpFields), so the
inputs cannot die while the header lives, and .source / .flags can read
pattern_ptr / flags_ptr directly. If that holds, the table can go: half the
inserts, half the removals, half the rekeys, and one fewer thing for the
post-minor walk to carry.
REGEX_POINTERS is not removable the same way — it is the registry the
copied-minor finaliser enumerates to find dead headers, so deleting it needs
finalization to be driven from somewhere else (or made unnecessary, which the
next item points at).
Care required: js_regexp_construct reads the table for a RegExp pattern
argument; RegExp.prototype.compile reassigns both fields; and WTF-8 patterns
with lone surrogates must keep reading the same bytes. This wants the regex
suite and test262's RegExp subset, not just a counter.
2. 72.3 MB of RegExpHeader per reply
header_bytes / new = 72 B, header_bytes = 72,270,792 for one 3300-character
reply. That is the nursery pressure #9845 deliberately moved in from the malloc
arm — a good trade, and it is now the largest single allocation this loop makes.
It is also what #9845 was charged with in I3 (+46…+63 MB peak at 3300).
Nothing about the object is observed in the hot path: g54.default() builds a
fresh /…/g per grapheme, the caller runs test() on it once, and the object
dies. Two directions, neither costed yet:
- Shrink the header. 72 bytes is three pointers, seven bools,
last_index,
magic, two program pointers and meta. magic (8 bytes) is a
duplicate-runtime identity fallback; the seven bools are seven bits.
- Do not allocate it. This is escape analysis on a literal whose object
never escapes the expression that tests it — a compiler change, and the same
shape as the segment-view work. Nothing may pattern-match the specific
library, per the campaign's rule about laundering a misclassification.
Why this is filed rather than done
Both are the next bites after the current regex stack (#9891, #9892) settles,
and both are bigger than they look: item 1 changes .source semantics' source
of truth, item 2 is either an ABI change or a compiler pass. The counters are
here so whoever takes them starts from a number instead of a profile — and so
the claim that they are worth taking can be checked before the work, per the
campaign's own rule that a profile ranks time while a counter ranks executions.
Two load-independent counters from the I6 cc arm say what a
RegExpconstruction still costs after #9845 (header in the nursery), #9819 (flags
without allocation), #9891 (the write-barrier gate) and #9892 (the literal-site
key). One 3300-character reply, perrymaster, quiet box,
/root/rig9831/regexdiag_I6_3300.txt:1. Two address-keyed side-table inserts per construction — 2.0 M per reply
side_table_inserts / new = 2.00exactly. Everyjs_regexp_newdoes:REGEX_POINTERS.with(|s| s.borrow_mut().insert(ptr))— aPtrHashSetinsert;
REGEX_SOURCE_TABLE.with(|t| t.borrow_mut().insert(ptr, (pattern, flags)))—a
PtrHashMapinsert carrying twoArc<str>.Each is a
PtrHasherhash plus a hashbrown insert, and each is mirrored by aremoval at death and a rekey per evacuation (
regex_header_moved_for_gc,regex_header_clear_dead_for_gc). Both tables are also walked in full afterevery copied minor by
finalize_dead_copied_minor_from_space_regexps, whosecost is documented as O(live headers + headers allocated since the last minor)
— and this workload allocates a million of them per reply.
Profile corroboration, from the segment-loop probe (region B, 60,000 reps,
sample, main thread, parser self-check leaf sum == thread header exactly):PtrHasherinserts underjs_regexp_newregex_header_clear_dead_for_gc(the two removals + expando clear)(That capture is on a pre-#9845 binary, so the removals sat in the malloc sweep;
on main the same work is in the copied-minor finaliser. The inserts are
unchanged either way.)
The design lead, and it is a subtraction rather than a speed-up.
REGEX_SOURCE_TABLEexists (#637) because the header'spattern_ptr/flags_ptrwere raw pointers into strings the GC could free — the header wasgc_malloc'd and untraced. Since #9845 the header is an ordinary GC objectwhose two string slots are traced (
GcLayoutSlotKind::RegExpFields), so theinputs cannot die while the header lives, and
.source/.flagscan readpattern_ptr/flags_ptrdirectly. If that holds, the table can go: half theinserts, half the removals, half the rekeys, and one fewer thing for the
post-minor walk to carry.
REGEX_POINTERSis not removable the same way — it is the registry thecopied-minor finaliser enumerates to find dead headers, so deleting it needs
finalization to be driven from somewhere else (or made unnecessary, which the
next item points at).
Care required:
js_regexp_constructreads the table for a RegExp patternargument;
RegExp.prototype.compilereassigns both fields; and WTF-8 patternswith lone surrogates must keep reading the same bytes. This wants the regex
suite and test262's RegExp subset, not just a counter.
2. 72.3 MB of
RegExpHeaderper replyheader_bytes / new = 72 B,header_bytes = 72,270,792for one 3300-characterreply. That is the nursery pressure #9845 deliberately moved in from the malloc
arm — a good trade, and it is now the largest single allocation this loop makes.
It is also what #9845 was charged with in I3 (+46…+63 MB peak at 3300).
Nothing about the object is observed in the hot path:
g54.default()builds afresh
/…/gper grapheme, the caller runstest()on it once, and the objectdies. Two directions, neither costed yet:
last_index,magic, two program pointers andmeta.magic(8 bytes) is aduplicate-runtime identity fallback; the seven bools are seven bits.
never escapes the expression that tests it — a compiler change, and the same
shape as the segment-view work. Nothing may pattern-match the specific
library, per the campaign's rule about laundering a misclassification.
Why this is filed rather than done
Both are the next bites after the current regex stack (#9891, #9892) settles,
and both are bigger than they look: item 1 changes
.sourcesemantics' sourceof truth, item 2 is either an ABI change or a compiler pass. The counters are
here so whoever takes them starts from a number instead of a profile — and so
the claim that they are worth taking can be checked before the work, per the
campaign's own rule that a profile ranks time while a counter ranks executions.