Skip to content

Replace flag-proven casts with Unsafe.As on hot arithmetic paths#2673

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-unsafe-as-arith
Jul 13, 2026
Merged

Replace flag-proven casts with Unsafe.As on hot arithmetic paths#2673
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-unsafe-as-arith

Conversation

@lahma

@lahma lahma commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Idiom-consistency cleanup: the codebase's established pattern for flag-proven downcasts is Unsafe.As<T> after an InternalTypes test (~20 sites in ObjectInstance / JintMemberExpression), but the hot arithmetic paths still used plain (T) casts that emit CastHelpers.ChkCastClass. This applies the idiom to the sites where the dominating branch provably establishes the type:

  • (JsNumber) pairs in the Plus/Minus/Times/Divide double lanes (_type == InternalTypes.Number on both operands)
  • (JsBoolean) on Compare(...) results in the relational classes — the conversion is justified by a closed audit of Compare's return set (every path returns JsBoolean.True/False or Undefined; comment added at each site)
  • (JsNumber) in JintExpression.CompareNumber (sole caller guards IsNumber() on both args; Debug.Assert precondition added) and JintUnaryExpression.EvaluateMinus
  • JsValueExtensions.AsNumber — its own IsNumber() guard dominates the cast, so misuse still throws the same ArgumentException

Deliberately skipped (not provably safe):

  • Exponentiate's (JsNumber) casts — guarded only by AreNonBigIntOperands, which proves not BigInt; JsNumber-ness rests on an inter-procedural ToNumeric contract, not a same-variable check
  • JsValueExtensions.AsInteger — 34 of 35 call sites are flag-proven, but the public IConvertible.ToInt32 implementation (JsValue.Convertible.cs) calls it unguarded; today that's an InvalidCastException, with Unsafe.As it would be type confusion. Left unchanged.

Impact

Benchmarks on the arithmetic set (stopwatch, math-cordic, string-fasta, crypto-md5) are neutral — within the ±2% machine drift band, as expected for a change whose entire budget was 0.7% of profile self time (much of which the recent lane work already bypassed). ETW confirms the mechanism: ChkCastClass self time 48.7 → 35.7 ms on the mixed SunSpider driver for identical work.

Gates

  • Jint.Tests: 3541 (net10.0) + 3478 (net472) passed, 0 failed
  • Jint.Tests.PublicInterface: 82 × 2 TFMs passed
  • Jint.Tests.CommonScripts: green both TFMs
  • Test262: 99,429 passed, 0 failed

🤖 Generated with Claude Code

ETW profiling showed CastHelpers.ChkCastClass at ~0.7% on hot
interpreter paths. The JIT cannot correlate an InternalTypes flag
test with the runtime type, so a plain (T) cast after such a check
still emits a castclass. Apply the established Unsafe.As-after-
flag-check idiom (see ObjectInstance shape-mode paths) to the
enumerated hot arithmetic sites:

- JintBinaryExpression: (JsNumber) operand casts in the
  Plus/Minus/Times/Divide double lanes, each directly dominated by
  left/right._type == InternalTypes.Number checks; (JsBoolean)
  casts on Compare(...) results in the four relational classes,
  guarded by the not-undefined check (Compare's return set is
  closed: JsBoolean.True/False or undefined on every path).
- JintExpression.CompareNumber: (JsNumber) casts; sole caller
  Compare has proven both operands via IsNumber(), documented
  with a Debug.Assert precondition.
- JintUnaryExpression.EvaluateMinus: (JsNumber) cast after the
  post-ToNumeric IsNumber() check.
- JsValueExtensions.AsNumber: cast after the method's own
  IsNumber() guard (throw helper is [DoesNotReturn]), with a
  Debug.Assert beside it.

AsInteger is intentionally left unchanged: it has no in-method
guard and IConvertible.ToInt32 (JsValue.Convertible.cs) invokes it
unguarded on arbitrary JsValues, so Unsafe.As there would turn a
user-visible InvalidCastException into type confusion. The
Exponentiate (JsNumber) casts are likewise skipped: their guard
(AreNonBigIntOperands) only proves not-BigInt; JsNumber-ness rests
on callers' ToNumeric contract rather than a local flag check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma force-pushed the perf-unsafe-as-arith branch from 5eee5d9 to d9c7882 Compare July 13, 2026 13:59
@lahma
lahma enabled auto-merge (squash) July 13, 2026 13:59
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