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:
- .NET interop — boxed
object/RuntimeValue interchange, reflective dispatch, and the $Runtime/$Array/$TSFunction host types stay usable from .NET.
- 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.
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:object/RuntimeValueinterchange, reflective dispatch, and the$Runtime/$Array/$TSFunctionhost types stay usable from .NET.microsoft/TypeScriptconformance 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)
slocal typedobject→ dynamic$Runtime::Add+ O(n²) fresh-string concat$TSFunctionbuild viaGetMethodFromHandlereflection (slower than our own interpreter!)List<object>; inlineisinstladder + box per element opDictionary<string,object>+ string-hash + box per fieldHeadline: 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$Runtimehelpers.Children (rough priority order — leverage vs risk)
object(shared root cause). Highest leverage; unblocks strings + arrays.objectis a deliberate constraint (aliasing / reassignment / RuntimeValue migration) before changing.$TSFunctionper call. Fixes the worst anomaly (compiled closures slower than interpreted). Partial machinery exists (TryEmitArrowAsDelegate).isinstladder + boxing). Follows from F4.MethodInfo+ delegate-specialize. array-methods.Dictionary<string,object>. Biggest refactor, smallest current offender; defer.How to reproduce the measurements
Constraints / non-negotiables for any child PR
dotnet test,SharpTS.Test262, andSharpTS.TypeScriptConformance(no baseline regression).$Runtime,$Array,$TSFunction) remain reflectively usable.