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); } ///