From b6a2c59a319397a09eceae8143c0816e286a791b Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 20 Jun 2026 20:31:16 -0700 Subject: [PATCH] Perf #859: elide per-char box round-trip in promoted-string charCodeAt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EmitPromotedStringCharCodeAt` boxed its `double` result (both the in-range `get_Chars` value and the OOB `NaN`), so a numeric consumer like `sum + s.charCodeAt(i)` immediately unboxed it via `$Runtime::ConvertToNumber` — a dead box→unbox round-trip, and the `box Double` is a heap allocation *per character* in a scan loop. Both branches already push a raw `double`, so the merge is type-consistent without boxing: drop the two `box Double` and set `StackType.Double`. The consumer's `EnsureDouble` then no-ops (it already checks StackType), and a boxed-object consumer re-boxes once via `EmitBoxIfNeeded`. This is the safe case for box-elision because the promoted charCodeAt is a self-contained typed path with no dynamic-fallback merge — unlike the general `charCodeAt` (whose box is required to converge with the `InvokeMethodValue` fallback) and the count-primes array read (whose box merges the in-range value with the OOB `$Undefined` branch); those are structural, not dead, and are left untouched. **strings @10k: ~0.20ms → ~0.12ms — now ~parity with Node (~0.11ms)**, was ~2.4×. Eliminating the per-char box allocation removed the dominant residual GC cost. No IL-buffering layer exists, so a general peephole-rewrite pass isn't feasible without refactoring; this is the targeted source-level (StackType-propagation) form. Broader box/unbox elision (a `valueNeeded` discard flag) and loop-invariant `get_Length` hoisting are deferred — lower value now that the typed work removed most round-trips, higher risk. Green on dotnet test (14004; existing StringAccumulatorPromotionTests cover the charCodeAt scan + OOB→NaN paths in both modes) except the pre-existing stale/flaky Test262 baselines. IL-verified. --- Compilation/ILEmitter.Properties.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Compilation/ILEmitter.Properties.cs b/Compilation/ILEmitter.Properties.cs index 4db7a8e7..54920777 100644 --- a/Compilation/ILEmitter.Properties.cs +++ b/Compilation/ILEmitter.Properties.cs @@ -1276,15 +1276,17 @@ private void EmitPromotedStringCharCodeAt(LocalBuilder sb, List arguments) IL.Emit(OpCodes.Ldloc, idxLocal); IL.Emit(OpCodes.Callvirt, getChars); IL.Emit(OpCodes.Conv_R8); - IL.Emit(OpCodes.Box, _ctx.Types.Double); IL.Emit(OpCodes.Br, end); IL.MarkLabel(oob); IL.Emit(OpCodes.Ldc_R8, double.NaN); - IL.Emit(OpCodes.Box, _ctx.Types.Double); + // #859: leave a raw float64 (both branches push double) instead of boxing. The result is + // typically consumed by a numeric op (`sum + s.charCodeAt(i)`), whose EnsureDouble is then a + // no-op; a boxed-object consumer re-boxes via EmitBoxIfNeeded (which checks StackType). This + // elides the per-char `box Double` (a heap allocation) plus the consumer's `ConvertToNumber`. IL.MarkLabel(end); - SetStackUnknown(); + SetStackType(StackType.Double); } ///