From d7601c7d74b2e195c6554f5d85390edbdea069d7 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sat, 20 Jun 2026 22:10:09 -0700 Subject: [PATCH] Perf #856: inline volatile cancellation check at loop backedges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-iteration `call $Runtime.CheckCancellation()` emitted at every loop backedge sat in every loop body as an optimization barrier: RyuJIT won't inline CheckCancellation (it contains newobj+throw), so the bare call was measured at ~half the runtime of a tight numeric loop. Inline the field test on the hot path and only call the throwing helper on the cold cancel path: volatile. ldsfld _cancelRequested brfalse call CheckCancellation() // cold: only when cancelling The volatile. prefix is mandatory: _cancelRequested is loop-invariant, so a plain ldsfld could be hoisted out of the loop by LICM, reading the flag once and never re-checking — silently reintroducing the #74 async-hang. Volatile forbids the hoist at zero measured cost. Results (branch vs main vs Node): factorial tight loop 1027 -> 665 ms (1.6x; Node 210) count-primes @100k 403 -> 359 ms (1.12x; Node 264) Correctness: compiled while(true) unwinds with OperationCanceledException the instant _cancelRequested is tripped via reflection (the #74 harness path); vm-timeout tests pass; Test262 Timeout=0. Loop-backedge emitter only; the dynamic-invocation recursion guard (EmitStackGuard) is left as-is. --- Compilation/StatementEmitterBase.cs | 33 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/Compilation/StatementEmitterBase.cs b/Compilation/StatementEmitterBase.cs index fbe07a98..b7813784 100644 --- a/Compilation/StatementEmitterBase.cs +++ b/Compilation/StatementEmitterBase.cs @@ -365,16 +365,37 @@ protected virtual void EmitIf(Stmt.If i) } /// - /// Emits call $Runtime.CheckCancellation() at a loop backedge so + /// Honors the runner's cooperative cancellation flag at a loop backedge so /// compiled IL inside async/generator state machines (which inherit these - /// base loop emitters) honors the runner's cooperative cancellation flag. - /// See issue #74 — without this, a `while(true){}` inside an async function - /// hangs the test thread past the runner's timeout. + /// base loop emitters) can be unwound. See issue #74 — without this, a + /// `while(true){}` inside an async function hangs the test thread past the + /// runner's timeout. /// + /// + /// Perf (#856): instead of an unconditional call $Runtime.CheckCancellation(), + /// we inline the field test and only call the (throwing) helper on the cold + /// cancel path. RyuJIT will not inline CheckCancellation itself (it + /// contains newobj+throw), so the bare call sat in every loop + /// body as a per-iteration optimization barrier — measured at ~half the + /// runtime of a tight numeric loop. Inlining the test recovers ~1.6×. + /// + /// The volatile. prefix is mandatory: _cancelRequested is + /// loop-invariant, so a plain ldsfld could be hoisted out of the loop + /// by RyuJIT's LICM — reading the flag once and never re-checking, silently + /// reintroducing the #74 hang. The volatile read forbids that hoist and was + /// measured to cost nothing here. + /// protected void EmitCancellationCheck() { - if (Ctx.Runtime?.CheckCancellationMethod != null) - IL.Emit(OpCodes.Call, Ctx.Runtime.CheckCancellationMethod); + if (Ctx.Runtime?.CheckCancellationMethod == null || Ctx.Runtime?.CancelRequestedField == null) + return; + + var notCancelled = IL.DefineLabel(); + IL.Emit(OpCodes.Volatile); + IL.Emit(OpCodes.Ldsfld, Ctx.Runtime.CancelRequestedField); + IL.Emit(OpCodes.Brfalse, notCancelled); + IL.Emit(OpCodes.Call, Ctx.Runtime.CheckCancellationMethod); + IL.MarkLabel(notCancelled); } ///