Skip to content

Harden and optimize RegExp execution#543

Open
zzjjob wants to merge 4 commits into
bellard:masterfrom
zzjjob:perf/regexp-optimization-origin
Open

Harden and optimize RegExp execution#543
zzjjob wants to merge 4 commits into
bellard:masterfrom
zzjjob:perf/regexp-optimization-origin

Conversation

@zzjjob

@zzjjob zzjjob commented Jul 16, 2026

Copy link
Copy Markdown

Depends on #541. This branch starts at the exact head commit of #541 (a2c571acad9a4079fc0e29aacc4da7bbef94d4ff). Once #541 is merged, this PR reduces to three RegExp-only commits and 11 changed files. No Date optimization is included.

Summary

  • validate serialized function and RegExp bytecode, metadata, branch targets, operands, captures, ranges, and group-name trailers before execution
  • add bounded, versioned RegExp optimization metadata for literals, prefixes, quick checks, scan entries, and leading-character candidates
  • accelerate raw and escaped literals, conservative required prefixes, complex-branch candidate filtering, and global replacement
  • reduce replacement-template parsing and common capture-array allocation overhead
  • replace runtime opcode-shape probing with a compiler-produced execution descriptor while retaining the generic interpreter as the semantic fallback
  • refresh borrowed metadata on every RegExp.prototype.compile() transition
  • preserve interrupt semantics with bounded polling in every accelerated unbounded scan
  • keep interpreter and capture stacks configurable for constrained arm32/arm64 builds

Design and compatibility

Optimization metadata is treated as untrusted when it comes from serialized bytecode. Valid source-compiled and checked-QBC patterns use the same validated descriptor; patterns that cannot be proven eligible continue through lre_exec(). The implementation adds no JIT, executable memory, architecture-specific assembly, or shared mutable global state.

The binary-object version is bumped because the metadata layout changes. Old or malformed QBC is rejected cleanly instead of being interpreted with incompatible optimization state.

Performance

Fresh seven-run medians on macOS 15.7.4 arm64, Apple clang 17.0.0, -O2. The before build is the exact #541 head and the after build is this PR head. Times are ns/op; lower is better.

QuickJS microbench Before After Change
regexp_ascii 128.60 76.00 1.69x faster
regexp_utf16 130.65 76.60 1.71x faster
regexp_replace 651.50 188.40 3.46x faster
sum of the three medians 910.75 341.00 2.67x faster

The prefix and quick-check tiers were also measured against otherwise identical builds with their metadata emission disabled:

Candidate filter Late hit Miss
required prefix 2.86x faster 5.24x faster
multi-character quick check 2.44x faster 3.62x faster

ahaoboy/js-engine-benchmark

Benchmark commit: 81415304391884c41601d8e7fd4ebf1cbc6385e6. Scores are higher-is-better.

Suite Before After Change Runs
isolated RegExp 567 1723 3.04x 7-run median
RegExp in full suite 564 1685 2.99x 3-run median
full suite score 1821 2078 +14.11% 3-run median

The non-RegExp subtests remained broadly flat; the full-score increase is driven by the RegExp workload.

Serialized metadata size

A fixture with 1,000 raw literals of 256 code units measured:

Metric Before After Change
QBC size 1,364,423 bytes 1,116,423 bytes -18.2%
median RSS 5,439,488 bytes 4,358,144 bytes -19.9%

Correctness and hardening

  • RegExp.prototype.compile() transition matrix covers metadata-to-generic and metadata-to-metadata replacements, flags, lastIndex, captures, indices, replacement, failures, and GC stress
  • direct differential tests compare descriptor execution with the generic interpreter over deterministic randomized inputs
  • malformed metadata, opcodes, jumps, capture indexes, old versions, and recomputed-checksum QBC mutations are rejected before script execution
  • long 8-bit/UTF-16 atom, prefix, quick-check, leading-candidate, and global-replace scans test both interrupt continuation and timeout propagation
  • independent-runtime pthread stress verifies there is no cross-runtime shared state

Verification

  • make test on the exact official stacked branch
  • source and generated-QBC execution
  • Werror, ASan, UBSan, TSan, and macOS leak checks
  • 1,491 relevant fixed-commit Test262 RegExp/String integration tests passed with no error
  • Android armv7a/aarch64 and iOS device/simulator arm64 compilation
  • focused source and QBC hardening benchmarks remained within the 3% regression gate

This PR contains the RegExp changes derived from a6088bb2ad78365e84fd6955dc6fe14d523925cd, 63df7cc81b5136455971d9705aea94553c4d6fc4, and c2ca48b03498d3cf28b21d7a32a5d1b856b1dec0.

zzjjob added 4 commits July 9, 2026 16:09
Validate compiled RegExp programs before execution and reject malformed opcode streams, register access, jump targets, metadata, and forged atom flags. Verify RegExp constants while reading serialized function bytecode, rebuild trusted programs from their source, and bump BC_VERSION for the changed format.

Emit versioned optimization metadata for decoded literals, required prefixes, and multi-position quick checks. Use direct string searches for literal expressions, prefilter generic expressions before entering the backtracking interpreter, execute verified candidate offsets with lre_exec_at(), and avoid heap allocation for common capture vectors.

Add C coverage for the validator and metadata derivation, mutate QBC fixtures to exercise malformed RegExp and function bytecode rejection, extend source/QBC built-in tests, and add focused microbench cases for escaped literals, misses, prefixes, and quick checks.

Verification: make test

Benchmark on macOS arm64: the complete microbench total decreases by 35.79% for source and 35.60% for QBC; the 82-case geometric mean improves 1.175x and 1.168x, while the RegExp subset improves 2.765x and 2.792x. Non-RegExp results are broadly flat, with a reproducible 3-4.5% function-call microbenchmark regression retained for follow-up.
Move the standard unanchored search loop out of the backtracking bytecode so failed candidates can reuse the interpreter stack without repeatedly constructing the synthetic split/any/goto states.

Restore capture slots when a top-level candidate fails, increase the inline backtracking stack from 32 to 128 entries to avoid repeated reallocations, and recognize safe (^|X)literal prefixes. The prefix filter scans for up to four required literal characters while preserving the original interpreter path for ignore-case, multiline, sticky, and Unicode modes.

Add regression coverage for captures, lookarounds, Unicode advancement, global replacement, sticky matching, bounded leading alternations, wide strings, escaped candidates, and fallback flag combinations.

Benchmark: ahaoboy/js-engine-benchmark RegExp median improves from 862 to 1771 (+105.5%); the re17 hotspot drops from about 325 ms to 17 ms, and the full-suite score improves from 1938 to 2153 (+11.1%).

Verified with make test, source and checked-QBC execution, the RegExp bytecode validator, ASan, and UBSan.
Make the RegExp fast paths introduced by the previous optimization commits safe for production use.

Refresh the borrowed metadata view whenever compiled RegExp data is replaced, so RegExp.prototype.compile() cannot retain metadata owned by freed bytecode. Route all construction and recompilation paths through the same installer.

Preserve interrupt semantics in atom, prefix, quick-check, and leading-character scans by polling at bounded intervals and propagating timeouts through exec and global replace. Replace runtime opcode-shape probing with a compiler-produced metadata v3 execution descriptor, bound duplicated payloads, validate descriptor fields, and reject stale or malformed serialized bytecode.

Keep interpreter and capture stacks configurable for constrained arm32/arm64 targets, while retaining measured defaults. Centralize optimized exec/replace dispatch without adding shared mutable state.

Add compile transition and GC stress coverage, metadata differential and mutation tests, checked-QBC rejection cases, embedding-level interrupt tests, and independent-runtime pthread stress tests. Wire the new tests into make test and bump the QuickJS bytecode version for the metadata format change.

Verified with make test, Werror, ASan, UBSan, TSan, macOS leak checks, relevant Test262 coverage, and Android/iOS arm compilation. Benchmark medians remain within the release thresholds while long-literal QBC size and RSS are reduced.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant