Skip to content

regex: a construction still pays two address-keyed side-table inserts (2.0M/reply) and 72 MB of headers per reply #9908

Description

@proggeramlug

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.

Activity

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