From 67723f68b8353b2ad7d4ae920c9f7640a0996fae Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 20 Mar 2026 21:10:49 +0100 Subject: [PATCH 1/3] JIT: Null out exceptions in reused continuations On resumption we use the presence of an exception object as the indicator for whether an exception should be rethrown. With continuation reuse we should take care to reset this exception so that we do not rethrow the same exception on a future resumption. --- src/coreclr/jit/async.cpp | 20 ++++++++++ src/coreclr/jit/jitconfigvalues.h | 4 +- src/tests/async/regression/125805.cs | 51 ++++++++++++++++++++++++ src/tests/async/regression/125805.csproj | 5 +++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/tests/async/regression/125805.cs create mode 100644 src/tests/async/regression/125805.csproj diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 94f6b0e646675f..b09abcf0f75063 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -2871,6 +2871,16 @@ BasicBlock* AsyncTransformation::RethrowExceptionOnResumption(BasicBlock* GenTree* storeException = m_compiler->gtNewStoreLclVarNode(exceptionLclNum, exceptionInd); LIR::AsRange(resumeBB).InsertAtEnd(LIR::SeqTree(m_compiler, storeException)); + if (ReuseContinuations()) + { + // If we may reuse this continuation later then make sure we don't see the same exception again. + GenTree* continuation = m_compiler->gtNewLclvNode(m_compiler->lvaAsyncContinuationArg, TYP_REF); + unsigned exceptionOffset = OFFSETOF__CORINFO_Continuation__data + layout.ExceptionOffset; + GenTree* null = m_compiler->gtNewNull(); + GenTree* nullException = StoreAtOffset(continuation, exceptionOffset, null, TYP_REF); + LIR::AsRange(resumeBB).InsertAtEnd(LIR::SeqTree(m_compiler, nullException)); + } + GenTree* exception = m_compiler->gtNewLclVarNode(exceptionLclNum, TYP_REF); GenTree* null = m_compiler->gtNewNull(); GenTree* neNull = m_compiler->gtNewOperNode(GT_NE, TYP_INT, exception, null); @@ -3244,6 +3254,16 @@ void AsyncTransformation::CreateResumptionsAndSuspensions() // bool AsyncTransformation::ReuseContinuations() { +#ifdef DEBUG + static ConfigMethodRange s_range; + s_range.EnsureInit(JitConfig.JitAsyncReuseContinuationsRange()); + + if (!s_range.Contains(m_compiler->info.compMethodHash())) + { + return false; + } +#endif + return JitConfig.JitAsyncReuseContinuations() != 0; } diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index e5cfe83d7ac427..5f04e291d98af5 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -592,9 +592,11 @@ OPT_CONFIG_INTEGER(JitDoOptimizeMaskConversions, "JitDoOptimizeMaskConversions", OPT_CONFIG_INTEGER(JitOptimizeAwait, "JitOptimizeAwait", 1) // Perform optimization of Await intrinsics OPT_CONFIG_STRING(JitAsyncDefaultValueAnalysisRange, "JitAsyncDefaultValueAnalysisRange") // Enable async default value analysis based on method hash range +// Enable continuation reuse based on method hash range +OPT_CONFIG_STRING(JitAsyncReuseContinuationsRange, "JitAsyncReuseContinuationsRange") // Save and reuse continuation instances in runtime async functions. Also // implies use of shared continuation layouts for all suspension points. -RELEASE_CONFIG_INTEGER(JitAsyncReuseContinuations, "JitAsyncReuseContinuations", 0) +RELEASE_CONFIG_INTEGER(JitAsyncReuseContinuations, "JitAsyncReuseContinuations", 1) RELEASE_CONFIG_INTEGER(JitEnableOptRepeat, "JitEnableOptRepeat", 1) // If zero, do not allow JitOptRepeat RELEASE_CONFIG_METHODSET(JitOptRepeat, "JitOptRepeat") // Runs optimizer multiple times on specified methods diff --git a/src/tests/async/regression/125805.cs b/src/tests/async/regression/125805.cs new file mode 100644 index 00000000000000..7ab3a8f849a4d7 --- /dev/null +++ b/src/tests/async/regression/125805.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +public class Runtime_125805 +{ + [Fact] + public static void TestEntryPoint() + { + ExceptionReuse().GetAwaiter().GetResult(); + } + + private static async Task ExceptionReuse() + { + try + { + await Throws(); + Assert.Fail("Expected throw"); + } + catch (Exception) + { + } + + try + { + await NoThrow(); + } + catch (Exception ex) + { + Assert.Fail("Did not expect throw: " + ex); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task Throws() + { + await Task.Yield(); + throw new Exception(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task NoThrow() + { + await Task.Yield(); + } +} + diff --git a/src/tests/async/regression/125805.csproj b/src/tests/async/regression/125805.csproj new file mode 100644 index 00000000000000..3fc50cde4b3443 --- /dev/null +++ b/src/tests/async/regression/125805.csproj @@ -0,0 +1,5 @@ + + + + + From 42152ee546536b0a89d58ca7c146e71bb761694b Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Fri, 20 Mar 2026 22:59:26 +0100 Subject: [PATCH 2/3] Run jit-format --- src/coreclr/jit/async.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index b09abcf0f75063..936d1c19976037 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -2876,8 +2876,8 @@ BasicBlock* AsyncTransformation::RethrowExceptionOnResumption(BasicBlock* // If we may reuse this continuation later then make sure we don't see the same exception again. GenTree* continuation = m_compiler->gtNewLclvNode(m_compiler->lvaAsyncContinuationArg, TYP_REF); unsigned exceptionOffset = OFFSETOF__CORINFO_Continuation__data + layout.ExceptionOffset; - GenTree* null = m_compiler->gtNewNull(); - GenTree* nullException = StoreAtOffset(continuation, exceptionOffset, null, TYP_REF); + GenTree* null = m_compiler->gtNewNull(); + GenTree* nullException = StoreAtOffset(continuation, exceptionOffset, null, TYP_REF); LIR::AsRange(resumeBB).InsertAtEnd(LIR::SeqTree(m_compiler, nullException)); } From de811bd32feed7bde26b8ab6fc630c43b19d9a6b Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Sat, 21 Mar 2026 13:55:03 +0100 Subject: [PATCH 3/3] Feedback --- src/tests/async/regression/125805.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/async/regression/125805.cs b/src/tests/async/regression/125805.cs index 7ab3a8f849a4d7..3f2f0e043ed2c3 100644 --- a/src/tests/async/regression/125805.cs +++ b/src/tests/async/regression/125805.cs @@ -13,7 +13,7 @@ public static void TestEntryPoint() { ExceptionReuse().GetAwaiter().GetResult(); } - + private static async Task ExceptionReuse() { try @@ -21,7 +21,7 @@ private static async Task ExceptionReuse() await Throws(); Assert.Fail("Expected throw"); } - catch (Exception) + catch (NullReferenceException) { } @@ -39,7 +39,7 @@ private static async Task ExceptionReuse() private static async Task Throws() { await Task.Yield(); - throw new Exception(); + throw new NullReferenceException(); } [MethodImpl(MethodImplOptions.NoInlining)]