Skip to content

Perf: statically-typed locals emitted as CLR object (shared root cause) #857

Description

@nickna

Part of #856 (perf epic). Likely the single highest-leverage fix — shared root cause behind the strings (117×) and array (28×/23×) gaps.

Summary

In compiled mode, locals declared with a concrete static type are emitted with CLR type object, discarding the type. Because the IL local is object, every downstream operation is forced through a dynamic $Runtime helper (string concat → $Runtime::Add(object,object), array index → an isinst dispatch ladder, etc.) plus box/unbox, even though the type is statically known.

Evidence (emitted IL)

function stringWork(n){ let s:string=""; for(...) s = s + "ab"; ... }

.locals init ( [0] object, ... )          // <-- `s` is `object`, not `string`
...
IL_001e: ldloc.0
IL_001f: ldstr "ab"
IL_0024: call object $Runtime::Add(object, object)   // dynamic add: must type-check both operands every iteration

function countPrimes(n){ const isPrime:boolean[]=[]; ... }

.locals init ( [0] object, ... )          // <-- `isPrime` is `object`, not List<bool>/bool[]

Compare: factorial/fibonacci, whose locals are typed float64, hit ~Node parity. The difference is entirely the local's CLR type.

Why this matters

  • string local → + can lower to String.Concat(string,string) and .length to a direct get_Length (no UnwrapStringReceiver).
  • List<bool>/List<double> local → element get/set lowers to direct get_Item/set_Item (kills the F3 ladder + boxing).

Investigate first (do NOT just "type the local")

The crucial unknown: is object deliberate or just unfinished? Candidate reasons to rule out before changing:

  • The local may be reassignable to a different runtime type, or aliased into object-typed slots.
  • The ongoing RuntimeValue boxing-elimination migration may intentionally defer this.
  • null/undefined/$Undefined placeholder values may need to flow through the same slot.

Where to look: the compiled local-slot type assignment (search DeclareLocal/LocalBuilder allocation in Compilation/, the numeric-slot taint pass referenced in prior work, and TypeProvider.cs). Determine the constraint, then specialize where provably safe (single static type, no cross-type reassignment, no escape into object slots).

Acceptance

  • strings + count-primes locals emit concrete CLR types where provably monomorphic.
  • No regression in dotnet test, Test262, TS conformance.

Scope / priority

High leverage, medium-high risk (touches the closure/capture/array hot paths). Gate on full suites.

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime/codegen performance work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions