Skip to content

Fuse strict-equality guards: x === undefined/null and typeof x === "literal"#2658

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-quickjs-ws2-strict-eq-fusion
Jul 13, 2026
Merged

Fuse strict-equality guards: x === undefined/null and typeof x === "literal"#2658
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-quickjs-ws2-strict-eq-fusion

Conversation

@lahma

@lahma lahma commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Third PR of the quickjs-ng learnings campaign (#2654 lanes, #2656 for-in array fast path). QuickJS dedicates opcodes (is_undefined, is_null, typeof_is_undefined, typeof_is_function) to these guard idioms, produced by its peephole pass; this is the AST-interpreter equivalent — a build-time specialized node.

What

When one side of ===/!== is statically known, the generic strict-equality node (evaluate both operands, generic dispatch) is replaced at build time:

  • x === undefined / x === null (either orientation): evaluates only the interesting operand; the answer is a single internal-type test. The undefined form keys off the same compile-time folding the identifier itself already evaluates through (BindingName.CalculatedValue), so semantics match the unfused path exactly. Strict equality against these singletons is precisely a type-identity check (document.all-style IsHTMLDDA values are only loosely equal to them, so the strict path needs no carve-out).
  • typeof x === "literal": the typeof side still evaluates through JintTypeOfExpression — unresolvable identifiers and host-object classification untouched — and since typeof only ever produces the interned JsString singletons, the comparison is a reference test against the singleton the literal maps to at build time. An unmapped literal ("bogus") can never match, but the operand still evaluates for its side effects.

Literal-vs-literal expressions keep the existing constant fold; loose equality (== null) is untouched.

Numbers

New GuardComparisonBenchmark (LCG-mixed inputs the branch predictor cannot memorize), default job:

Lane Before After Δ
IsUndefinedGuard 13.54 ms 10.36 ms −23.5%
IsNullGuard 12.42 ms 10.29 ms −17.1%
TypeofStringGuard 17.06 ms 14.24 ms −16.5%
TypeofUndefinedGuard 11.87 ms 10.73 ms −9.6%
TypeofSwitchMixed (existing lane) 22.13 ms 18.65 ms −15.7%
LooseNullGuard (untouched path) 10.13 ms 10.26 ms noise

linq-js is within run-to-run noise (its Execute lane varies ±5% between identical binaries).

Tests

New GuardFusionTests pin: both orientations, every typeof result string, undeclared-identifier typeof (no throw), impossible literals evaluating side effects exactly once, CLR interop callables classifying as "function", fused guards across await suspension, and the if-statement boolean fast path.

Full gate green: Jint.Tests, PublicInterface, CommonScripts, and a clean 99,429-test Test262 run.

🤖 Generated with Claude Code

…iteral"

QuickJS dedicates opcodes (is_undefined, is_null, typeof_is_*) to these
guard idioms; the AST equivalent is a build-time specialized node. When
one side of ===/!== is the undefined identifier, the null literal, or
the other side of a typeof expression is a string literal, the fused
node evaluates only the interesting operand and answers with a single
internal-type test - or, for typeof, a reference compare against the
interned singleton the literal maps to at build time (typeof only ever
produces those singletons; an unmapped literal can never match but the
operand still evaluates for its side effects).

The undefined form keys off the same compile-time folding the
identifier itself evaluates through (BindingName.CalculatedValue), the
typeof side still evaluates through JintTypeOfExpression (unresolvable
identifiers and host-object classification untouched), and
literal-vs-literal keeps the existing constant fold.

New GuardComparisonBenchmark (LCG-mixed inputs), default job:

| Lane                 |    Before |    After |        Δ |
|--------------------- |----------:|---------:|---------:|
| IsUndefinedGuard     |  13.54 ms | 10.36 ms |  -23.5%  |
| IsNullGuard          |  12.42 ms | 10.29 ms |  -17.1%  |
| TypeofStringGuard    |  17.06 ms | 14.24 ms |  -16.5%  |
| TypeofUndefinedGuard |  11.87 ms | 10.73 ms |   -9.6%  |
| TypeofSwitchMixed    |  22.13 ms | 18.65 ms |  -15.7%  |
| LooseNullGuard       |  10.13 ms | 10.26 ms | untouched|

linq-js within run-to-run noise (its Execute lane varies +-5% between
identical binaries).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma force-pushed the perf-quickjs-ws2-strict-eq-fusion branch from c195636 to 0fde311 Compare July 13, 2026 08:38
@lahma
lahma enabled auto-merge (squash) July 13, 2026 08:40
@lahma
lahma merged commit 97398a2 into sebastienros:main Jul 13, 2026
4 checks passed
@lahma
lahma deleted the perf-quickjs-ws2-strict-eq-fusion branch July 13, 2026 08:55
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