Skip to content

Add InternalTypes.Function flag to skip is-Function and env-cast checks on hot paths#2691

Closed
lahma wants to merge 1 commit into
sebastienros:mainfrom
lahma:perf-function-flag
Closed

Add InternalTypes.Function flag to skip is-Function and env-cast checks on hot paths#2691
lahma wants to merge 1 commit into
sebastienros:mainfrom
lahma:perf-function-flag

Conversation

@lahma

@lahma lahma commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two class-hierarchy casts run on hot call/reference paths and show up in ETW as CastHelpers:

  • callable is Function in per-call dispatch (JintCallExpression, Engine.Construct) — an IsInstanceOfClass on every call, distinguishing a JS function from other callables (a callable Proxy).
  • (Environment) reference.Base in environment-reference resolution (Engine.GetValue/PutValue, JintCallExpression, delete, binding init) — a ChkCastClass on every global/env variable access.

Both are now flag tests. InternalTypes.Function is added (free bit, 2^20) and set once in the Function base constructor — the single writer every function object (script/CLR/bound functions, built-in constructors) chains through. A callable Proxy derives from ObjectInstance, not Function, so it correctly never gets the flag and still dispatches through the ICallable path (verified).

Two shared helpers replace the casts with flag test + Unsafe.As:

  • JsValue.AsFunctionInstanceOrNull() — the flag-based equivalent of value as Function, used at the call/construct dispatch sites (and the existing throwing AsFunctionInstance now shares the same fast test).
  • Environment.FromReferenceBase(JsValue) — keyed on the ObjectEnvironmentRecord flag (single-sourced in the Environment ctor), keeping the checked cast as the mismatch fallback so any invariant violation still throws today's InvalidCastException.

Behavior is unchanged — the flags are proven single-writer, so Unsafe.As is safe. The two Engine call sites typed as ICallable (cold C#-invoke entry points, not the interpreter per-call path) are deliberately left as-is: converting them would need an interface→JsValue cast, defeating the purpose.

Benchmarks

~2% on a call/reference-dispatch-heavy loop (interleaved wall clock, branch 3.20/3.14 s vs baseline 3.24 s — both branch runs below baseline); neutral on non-call-dominated workloads. The change targets the per-call is Function and per-reference (Environment) casts specifically; SunSpider scripts and micro-loops that don't churn calls/references are unaffected.

(Absolute BDN numbers are omitted: this machine is thermally drifting ±40% run-to-run after extended benchmarking, which swamps a ~2% effect. The interleaved wall-clock A/B/A is the reliable read; the mechanism — removing the per-call/per-reference class-casts — is the justification, in the spirit of #2673.)

Gates

  • Jint.Tests: 3,597 (net10.0) + 3,534 (net472) passed, 0 failed
  • Jint.Tests.PublicInterface: 114 × 2 TFMs passed
  • Jint.Tests.CommonScripts: 28 × 2 TFMs passed
  • Test262: 99,429 passed, 0 failed
  • Semantics smoke: normal/method/arrow/bound calls, new construction, callable-Proxy and apply-trap dispatch (Function-flag discrimination), instanceof Function, recursion, built-in constructor dispatch, with/env-reference assignment and delete — all correct

🤖 Generated with Claude Code

Per-call dispatch used `callable is Function` (CastHelpers.IsInstanceOfClass)
to distinguish a JS function from other callables, and environment-reference
resolution cast the reference base with `(Environment)` (ChkCastClass) on
every global/env variable access - both class-hierarchy checks on hot paths.

Add InternalTypes.Function, set once in the Function base constructor (the
single writer every function object chains through; a callable Proxy derives
from ObjectInstance and correctly never gets it). Two shared helpers replace
the checks with a flag test plus Unsafe.As: JsValue.AsFunctionInstanceOrNull
for the call/construct dispatch and Environment.FromReferenceBase for the
reference-resolution sites, the latter keeping the checked cast as the
mismatch fallback so a violated invariant still throws as before. The
existing AsFunctionInstance now shares the same fast test.

~2% on call-dispatch-heavy loops (interleaved wall clock); neutral elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma

lahma commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: clean-machine BenchmarkDotNet A/B (after removing background load) shows this is a consistent ~2-3.7% regression on call dispatch (FunctionCalls +2.0%, MethodCalls +2.8%, Fib +3.6%), not the win an earlier noisy wall-clock read suggested. On a monomorphic call site the JIT lowers \callable is Function\ to a cheap predicted type-handle compare; the flag-test + Unsafe.As replaces that with a _type load + mask + branch, which is more work. The InternalTypes.Function flag doesn't pay off for the hot dispatch check. Not merging.

@lahma lahma closed this Jul 15, 2026
@lahma
lahma deleted the perf-function-flag branch July 15, 2026 07:44
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