Skip to content

Perf epic: close the compiled-output gap to Node (without regressing interop or conformance) #856

Description

@nickna

Goal

Make SharpTS compiled output meet or exceed Node.js on the cross-runtime benchmark suite (benchmarks/scripts/), without regressing two things we equally care about:

  1. .NET interop — boxed object/RuntimeValue interchange, reflective dispatch, and the $Runtime/$Array/$TSFunction host types stay usable from .NET.
  2. TypeScript language conformance — the Test262 and microsoft/TypeScript conformance baselines must not regress. The hot paths below (closures, captures, arrays) are exactly where this repo has had its thorniest correctness bugs (write-capture, block-scope shadows, per-iteration cells), so every codegen change here is gated on the full xUnit + conformance suites.

This is a map of where the time goes, derived from disassembling the emitted IL of each benchmark workload (ilspycmd -il). It is deliberately diagnosis-first: some children have a clear fix, others name where to investigate.

Where we stand (compiled vs Node, mean ms at largest size)

Benchmark Compiled Node Slowdown Dominant cost (from IL)
strings @10k 9.85 0.084 117× s local typed object → dynamic $Runtime::Add + O(n²) fresh-string concat
closures @100k 101.4 1.05 97× per-call $TSFunction build via GetMethodFromHandle reflection (slower than our own interpreter!)
count-primes @100k 29.8 1.07 28× array as List<object>; inline isinst ladder + box per element op
array-methods @100k 61.5 2.64 23× reflective callback dispatch per element; List↔$Array round-trips
objects @100k 4.2 0.30 14× object literal = Dictionary<string,object> + string-hash + box per field
factorial @10k 0.013 0.0097 1.35× ✅ near parity (tight numeric loop)
fibonacci @35 34.1 73.5 0.46× (we win) ✅ recursion/call core is already excellent

Headline: pure numeric/recursive codegen is already at or above Node (fib beats Node and Bun). Every gap is allocation + dynamic dispatch + boxing on values whose static types we already know. The single most common root cause is that statically-typed locals are emitted as CLR object, which forces every downstream op through dynamic $Runtime helpers.

Children (rough priority order — leverage vs risk)

How to reproduce the measurements

# cross-runtime numbers
benchmarks/run-benchmarks.ps1

# disassemble a workload's emitted IL
dotnet run -c Release -- --compile <script>.ts -o out.dll
ilspycmd -il out.dll      # inspect $Program::<fn>

Constraints / non-negotiables for any child PR

  • Green on dotnet test, SharpTS.Test262, and SharpTS.TypeScriptConformance (no baseline regression).
  • No SharpTS.dll hard-dependency introduced into standalone compiled output (see CLAUDE.md "Standalone DLL Constraint").
  • Interop types ($Runtime, $Array, $TSFunction) remain reflectively usable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicUmbrella tracking issue with child tasksperformanceRuntime/codegen performance work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions