Skip to content

Keep builtin-shape deopt lazy: unmaterialized function slots stay deferred#2588

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-lazy-builtin-deopt
Jul 6, 2026
Merged

Keep builtin-shape deopt lazy: unmaterialized function slots stay deferred#2588
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf-lazy-builtin-deopt

Conversation

@lahma

@lahma lahma commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

DeoptBuiltinShape previously materialized every slot when a shaped built-in fell back to dictionary mode, forcing all of the host's dispatcher functions into existence the moment user code added or removed one own property — Math.x = 1 instantiated all ~35 Math functions nobody asked for. For upcoming shaped hosts where deopt is routine (the global object deopts on any top-level var), an eager deopt would be strictly worse than the dictionary path it replaces, so this is a prerequisite for flipping GlobalObject/FunctionPrototype to shapes.

  • Already-materialized slots keep their descriptor instance — inline caches and spec identity depend on it.
  • Unmaterialized function slots become LazyBuiltinSlotDescriptor wrappers that create the dispatcher on first value read (mirroring LazyPropertyDescriptor, including clearing CustomJsValue once materialized).
  • Unmaterialized accessors (rare) still materialize eagerly — a data-descriptor wrapper cannot defer them.
  • Aliases share their target's instance — materialized or lazy, chains resolved — so pairs like Set.prototype.keys === Set.prototype.values keep one function identity across the deopt.
  • Own-key order and dictionary contents are otherwise byte-identical to the previous deopt.

Measurements

Deopt-triggering script (Math.x = 1; JSON.x = 1; Reflect.x = 1 + calls, fresh engine per iteration): 43.7 -> 32.3 KB/iter (-26%), matching the dispatcher functions no longer force-created.

No-regression gates (BenchmarkDotNet default jobs, A/B vs main + A/B/A noise check): BuiltinShapeBenchmark, EngineConstructionBenchmark, MinimalScriptBenchmark — allocations byte-identical on every row, times within the suites'' observed run-to-run bands (the deopt path is not on any of their execution paths).

Verification

  • Jint.Tests green (net10.0 + net472), Jint.Tests.PublicInterface green
  • Full test262: 99,260 passed, 0 failed
  • Alias identity, delete-after-deopt, enumeration order and getOwnPropertyNames verified against a live engine

🤖 Generated with Claude Code

…erred

DeoptBuiltinShape previously materialized EVERY slot, forcing all of a
host''s dispatcher functions into existence the moment user code added or
removed one own property (e.g. `Math.x = 1` instantiated all ~35 Math
functions). For upcoming shaped hosts where deopt is routine — the global
object deopts on any top-level var — an eager deopt would be strictly worse
than the dictionary path it replaces.

Now already-materialized slots keep their descriptor instance (inline
caches and spec identity depend on it); unmaterialized function slots
become LazyBuiltinSlotDescriptor wrappers that create the dispatcher on
first value read; unmaterialized accessors (rare) still materialize eagerly
since a data-descriptor wrapper cannot defer them; aliases share their
target''s instance — materialized or lazy — so name pairs keep one function
identity across the deopt. Like LazyPropertyDescriptor, the wrapper clears
CustomJsValue once materialized so it becomes a plain data descriptor.

Deopt-triggering script (Math/JSON/Reflect mutation, fresh engine):
43.7 -> 32.3 KB/iter (-26%). BuiltinShape/EngineConstruction/MinimalScript
suites: allocations byte-identical, times within run-to-run noise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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