perf25: regex/String construction, ToPrimitive, eval closures, null compare - #158
Merged
Merged
Conversation
compile.rs was at the 2000-line limit; the hoisted global reads and the sloppy fallback-global writes form one responsibility. No behavior change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`ch < "0" || ch > "9"` over a `charAt` result compiled to an unbox of `ch`, which deoptimized on its first iteration: string-validate-input's ZIP scan entered and deoptimized both of its loops on every call. A relational operator with a string-literal operand now takes the boxed comparison operation, which compares two strings by code unit and two numbers as numbers and deoptimizes on anything else (an object's `valueOf`). Only with a literal: `i < list.length` keeps the pass that reads the length as a scalar. The scan micro: 20.8k -> 7.3k instructions an iteration (QuickJS-NG 11.0k). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A regular-expression literal compiles to `new RegExp(pattern, flags)` with constant strings, and each evaluation went through the general construct path: a `prototype` property read, a hash-map-backed receiver, IsRegExp and ToString conversions that copied both strings, a SipHash lookup in the validation cache, and three property definitions that each allocated a key and a value. With `new.target` the constructor and string arguments none of that is observable, so the object is now built in one step with shared keys and the argument strings themselves, and the last validated pair is remembered by pointer. string-validate-input's literal went from 9455 to 3301 instructions per evaluation (QuickJS-NG 1723). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`new String(x)` took the general construct path: a `prototype` read, a receiver, a native call that converted the argument to an owned string and then defined the wrapper's two properties on hash-map storage. With `new.target` the constructor and a string or number argument nothing in between is observable, so the wrapper is now built in one step, like a RegExp from string arguments; both share `construct_intrinsic_directly`, which the general and wide-tier construct paths call. date-format-xparb's `String.leftPad` wrapper went from 1003 to 555 cycles (QuickJS-NG 665). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ToPrimitive read `@@toPrimitive` through the general symbol-property path on every object operand of `+`, cloning each prototype slot on the way to `undefined`. An ordinary object whose chain holds no such own property now answers that in one borrowed walk, the String-wrapper shortcut checks the receiver's own `valueOf` without cloning it, and the generic `+` joins two primitives through the same buffer-reusing concatenation the bytecode fast path uses instead of copying the left string first. `'0' + new String(5)` went from 646 to 444 cycles (QuickJS-NG 306). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every Date method looked its receiver's time value up twice: once to check that it is a Date and once to read it. The read is the check. `getTime` in a loop went from 296 to 183 cycles per call (QuickJS-NG 121). Also re-pins the hot-function order for the String, RegExp and ToPrimitive changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A function literal in direct eval code carried the eval's dynamic scope, which keeps every call of it off the slot-seeded direct path and the compact tiers: each call built a full interpreter frame and found even `String` by hashing the eval scope. When that code has no `eval` or `with` of its own, its scope is a fork nothing else writes and gains no name after the closure exists, so a closure that resolves none of its current names by name -- and has its own `this` and `arguments`, no `eval`, `with` or nested function -- now drops it. What such a closure reads is unchanged. date-format-xparb's generated format functions went from 7614 to 5785 cycles per call; the whole case went from 28.2k to 17.9k cycles per iteration (QuickJS-NG 17.6k). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`node.left == null` with an object operand fell through the environment-free comparison, cloned both operands and built a throwaway frame to run the general loose equality, only to learn that nothing but `null`, `undefined` and the IsHTMLDDA host object equals `null`. The environment-free helper now answers `null`/`undefined` against anything and number against number, and the wide tier's conditional jump asks it before cloning. binary-trees ran 3.3% fewer cycles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Extending `eval_binary_without_env` with the null and number cases made rustc re-partition the crate: the wide tier's dispatch loop came out 176 bytes smaller and crypto-md5 ran 2.5-4% more cycles at identical instructions (the formal run against main measured 1.042). The same comparison now lives in a small out-of-line helper the wide tier's conditional jump calls, the shared helper is as it was, and the loop is back to main's shape: md5 1.005 against main, binary-trees keeps its gain (0.976). Also pins `compact_fn::numeric_plan::run`, md5's hottest function, which floated with every edit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Semantic fast paths for the cases that were slowest against QuickJS-NG. Each one was found by splitting a case into micro pieces and comparing each piece against NG by its N-versus-2N delta.
new String(x): built directly as well (function::construct_intrinsic_directly).@@toPrimitivewalk on ordinary chains that lack it. The generic+now joins primitives in place.x == nullin the wide tier: answered without the general operator. The helper is kept out of the shared operator code, because extending that code re-rolled the dispatch loop and cost crypto-md5 4%.compact_fn::numeric_plan::runis now pinned.Formal run e34c35b against main 5110c21, 30 blocks, cycles (
target/comparison/perf23-e34c35b4-30b):Test262: the gap scans show 0 actionable gaps against QuickJS-NG for these slices:
Every commit passed
check.shin the pre-push hook. The unit record is in T033.🤖 Generated with Claude Code