-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improved async method code gen for exceptions #65863
Comments
cc: @davidwrighton, @janvorli, @davidfowl, @jcouv |
@tommcdon I'm curious what you think of the diagnostic impact |
cc: @mangod9 FYI |
I'd definitely be motivated on this if the numbers showed it to be worthwhile. Any benchmarks for this? Would def be curious how this helps for things like cancellation exceptions. Thanks |
Sure. I took this code: public class Original
{
public static async Task A() => await B();
private static async Task B() => await C();
private static async Task C() => await D();
private static async Task D() => await E();
private static async Task E() => throw new Exception();
} and decompiled it, then duplicated the resulting C# and modified the Original into a Proposal that tweaks the GetResult calls approximately as suggested. I then wrapped each in a benchmark: [Benchmark(Baseline = true)]
public async Task OriginalVersion()
{
try
{
await Original.A();
}
catch { }
}
[Benchmark]
public async Task ProposedVersion()
{
try
{
await Proposal.A();
}
catch { }
} So in each case, we have 6 async methods. In OriginalVersion, all 6 will throw/catch an exception. In ProposedVersion, A, B, C, and D will all propagate the exception in the proposed manner, so only 2 of the 6 will throw/catch an exception. Thus we end up removing ~66% of the exceptions being thrown/caught for this relatively small chain... longer chains would have more savings. This is obviously an approximation, as not all throw/catches have equivalent costs, and I've not added anything in here to assist with diagnostics (e.g. the Benchmarkusing BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
[MemoryDiagnoser]
public partial class Program
{
static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
[Benchmark(Baseline = true)]
public async Task OriginalVersion()
{
try
{
await Original.A();
}
catch { }
}
[Benchmark]
public async Task ProposedVersion()
{
try
{
await Proposal.A();
}
catch { }
}
}
//public class Original
//{
// public static async Task A() => await B();
// private static async Task B() => await C();
// private static async Task C() => await D();
// private static async Task D() => await E();
// private static async Task E() => throw new Exception();
//}
public class Original
{
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Ad__0 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = B().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Bd__1 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = C().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Cd__2 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = D().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Dd__3 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = E().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Ed__4 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private void MoveNext()
{
try
{
throw new OperationCanceledException();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
}
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(Ad__0))]
public static Task A()
{
Ad__0 stateMachine = default(Ad__0);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Bd__1))]
private static Task B()
{
Bd__1 stateMachine = default(Bd__1);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Cd__2))]
private static Task C()
{
Cd__2 stateMachine = default(Cd__2);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Dd__3))]
private static Task D()
{
Dd__3 stateMachine = default(Dd__3);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Ed__4))]
private static Task E()
{
Ed__4 stateMachine = default(Ed__4);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
}
// Actual version would use:
// awaiter.GetResult(out Exception e);
// if (e is not null)
// {
// ...
// }
// As that's not available, for benchmarking purposes it's instead using:
// Task originalTask = Unsafe.As<TaskAwaiter, Task>(awaiter);
// if (!Unsafe.As<TaskAwaiter, Task>(awaiter).IsCompletedSuccessfully)
// {
// Exception e = Unsafe.As<TaskAwaiter, Task>(awaiter).Exception.InnerException;
// ...
// }
// DO NOT DO THIS IN REAL CODE!!!!!!!!!!!
public class Proposal
{
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Ad__0 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
Exception propagated = null;
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = B().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
if (!Unsafe.As<TaskAwaiter, Task>(ref awaiter).IsCompletedSuccessfully)
{
propagated = Unsafe.As<TaskAwaiter, Task>(ref awaiter).Exception.InnerException;
goto Exceptional;
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
return;
Exceptional:
one__state = -2;
t__builder.SetException(propagated);
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Bd__1 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
Exception propagated = null;
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = C().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
if (!Unsafe.As<TaskAwaiter, Task>(ref awaiter).IsCompletedSuccessfully)
{
propagated = Unsafe.As<TaskAwaiter, Task>(ref awaiter).Exception.InnerException;
goto Exceptional;
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
return;
Exceptional:
one__state = -2;
t__builder.SetException(propagated);
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Cd__2 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
Exception propagated = null;
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = D().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
if (!Unsafe.As<TaskAwaiter, Task>(ref awaiter).IsCompletedSuccessfully)
{
propagated = Unsafe.As<TaskAwaiter, Task>(ref awaiter).Exception.InnerException;
goto Exceptional;
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
return;
Exceptional:
one__state = -2;
t__builder.SetException(propagated);
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Dd__3 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private TaskAwaiter u__1;
private void MoveNext()
{
Exception propagated = null;
int num = one__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = E().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (one__state = 0);
u__1 = awaiter;
t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = u__1;
u__1 = default(TaskAwaiter);
num = (one__state = -1);
}
if (!Unsafe.As<TaskAwaiter, Task>(ref awaiter).IsCompletedSuccessfully)
{
propagated = Unsafe.As<TaskAwaiter, Task>(ref awaiter).Exception.InnerException;
goto Exceptional;
}
awaiter.GetResult();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
return;
}
one__state = -2;
t__builder.SetResult();
return;
Exceptional:
one__state = -2;
t__builder.SetException(propagated);
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct Ed__4 : IAsyncStateMachine
{
public int one__state;
public AsyncTaskMethodBuilder t__builder;
private void MoveNext()
{
try
{
throw new OperationCanceledException();
}
catch (Exception exception)
{
one__state = -2;
t__builder.SetException(exception);
}
}
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(Ad__0))]
public static Task A()
{
Ad__0 stateMachine = default(Ad__0);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Bd__1))]
private static Task B()
{
Bd__1 stateMachine = default(Bd__1);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Cd__2))]
private static Task C()
{
Cd__2 stateMachine = default(Cd__2);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Dd__3))]
private static Task D()
{
Dd__3 stateMachine = default(Dd__3);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
[AsyncStateMachine(typeof(Ed__4))]
private static Task E()
{
Ed__4 stateMachine = default(Ed__4);
stateMachine.t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.one__state = -1;
stateMachine.t__builder.Start(ref stateMachine);
return stateMachine.t__builder.Task;
}
}
|
Oh wow. That's quite a benefit. I didn't realize there as a memory win here as well. This def seems like a worthwhile area to investigate.
This def seems important. Does the runtime have perf tests that exercise async? |
@jkotas as well. Assume this doesn't require any runtime changes? |
Just CoreLib, plus anything that might be needed in support of an efficient mechanism to augment the Exception with diagnostics information for the current stack frame. |
Yes, but all microbenchmarks. |
For reference, a simple async method like |
Would this even be relevant, if the green threads experiment succeeds? |
Yes. |
Thanks for including me on the discussion. I haven't read through all of the details yet, but on the surface it looks like this change might have impact on async debugging, and so we will need to evaluate impact to diagnostics. |
I think the primary diagnostics impact would be that it would no longer be possible to stop at each link along the chain of the async stack. In some cases, this might be an improvement actually. But, especially in a JMC scenario when a portion of the stack was non-user code, this could cause a significant regression. I think this could be addressed by making |
I am a bit pessimistic that the Support await'ing a Task without throwing #22144 proposal can go forward, without the
Both options are unsatisfactory. Especially the second, because the |
dotnet/csharplang#4565 Is a fuller proposal (compared to the discussion in dotnet/csharplang#4450 that is linked in the OP). Thanks @sharwell for linking. Could the solution proposed there be used in conjunction with this? Or is this expected to supersede everything async-throw related? As proposed here, canceling async functions would still be quite a bit more expensive than the proposal in dotnet/csharplang#4565 (1000x vs 2x), thanks to the Also, in this proposal, it suggests the compiler only emit this new code if the awaiter has the new As an aside, I do like the |
Is an AppendCurrentStackFrame call really necessary? |
I've definitely had to track down crashes related to cancellation tokens. I don't think not having stacks in that case would be ok :( |
Because of |
No. things like 'incorrect cancellation token used' also come up. Where we are doing semi-complex things with linked cancellation tokens and we end up returning the wrong cancellation token out of something. |
I understand that without a stack there are difficulties in debugging. |
The extra frame this proposal suggests adding to the exception's trace should be cheap. It's not a throw, nor is it a stack walk. It could be as simple as appending a const string generated by the compiler, similar to CallerMethodName, not even needing reflection of any kind. I don't see a good reason to special-case OperationCanceledException here, nor would I want to complicate code gen for it even more, nor sacrifice debugability. |
This is a very good solution, thank you |
Which benchmark, and after what optimization? |
Can you provide a trace. While cancellation is me expensive, it shouldn't ever be that bad. |
The benchmark in this issue demonstrates the performance advantage of moving from throwing an exception twice (current behavior) to once (new behavior). The benchmark in the linked issue shows the performance advantage of moving from throwing an exception once (current behavior) to not throwing an exception at all (new behavior). It's a slightly different scenario. In practice, both proposals have similar performance characteristics (i.e. many code situations will show noticeable benefits, while a subset of those will show exceptional benefits). |
If an awaitable stores the exception in an |
It occurred to me that So I propose to create StackFrame beforehand, and just append frame to current trace. This must be way faster! There are two ways ho we can do this:
Second option would create even better performance, but unsure if we want to create such a constructor, or to expose line number information in constants, if there are no debug symbols (Obfuscators, IDK, protection from cracking). Me personally don't care about hiding line numbers. |
The performance degradation caused by exceptions in asynchronous call chains can be catastrophic. We use an ASP.NET Core Middleware to handle exceptions uniformly, but this middleware is followed by several other middleware components, such as authentication and routing. Additionally, within MVC controllers, there are many layers of asynchronous calls. This results in more than ten layers of asynchronous operations between the point where an exception is thrown and the final exception handling module. When there are database fluctuations, exceptions can proliferate, drastically reducing system throughput. Under high concurrency, the lengthening of the thread pool's waiting queue and insufficient threads in the pool can lead to performance degradation of over 10,000 times. This is completely unacceptable, and due to this issue, we are considering migrating from .NET to the Java platform. |
@comm100-allon You may want to try .NET 9. Performance and scalability of exceptions have been significantly improved in .NET 9. The exception storms that you describe were primary motivating scenario. |
@comm100-allon You might want to use hand written exception handling Middleware, as current implementation is invoking whole middleware chain second time to route exception to exception handle controller. It looks like overkill for me. |
Exceptions have significant overhead and are for exceptional situations. The guidance then is to avoid them in performance-sensitive code. However, there are times when the cost of exceptions does matter, and while developers might be able to recognize such situations after the fact and do work to react to prevent performance-related issues there in the future, it'd be nice if the overhead associated with exceptions could be reduced. There is work happening in the .NET runtime to reduce the cost of exceptions significantly in general, but even with that work there will still be significant overhead, and that overhead is magnified by async. The async model for propagating exceptions involves throwing and catching an exception in every "stack frame" as part of the async call chain, so an exception propagating through a long chain of async methods can have an order (or two) of magnitude aggregate higher cost than the corresponding exception in a synchronous call stack.
This is a proposal to do better. There have been similar discussions over the years in a variety of issues/discussions on csharplang, but I'm hoping we can use this issue as the center of gravity and decide if/what to do about this (which could include deciding once and for all to do nothing).
Proposal
Today, for an async method like the following:
the compiler emits a MoveNext method containing scaffolding like the following:
That
awaiter.GetResult()
call throws an exception for a faulted awaitee, with that exception then getting caught by the compiler-generated catch, which stores the exception into the builder associated with this method, completing the operation. TheGetResult()
method is defined as part of the await pattern, a parameterless method whose return value is either void or matches theTResult
of the operation being awaited.Pattern
We update the await pattern to optionally include a method of the form:
where the
TResult
may bevoid
if the operation doesn't return a value. If an awaiter exposes aGetResult
of this form, it should not throw an exception even for failed operations. Instead, if the operation completes successfully, it should return a value as does the existingGetResult
method, with theexception
out argument set tonull
. If the operation completes unsuccessfully, it should returndefault(TResult)
, with theexception
out argument set to a non-null instance of anException
.Codegen
When an await is nested inside of any user-written
try
block, nothing changes.For any await outside of all user-written
try
blocks, if the awaiter exposes the newGetResult(out Exception? exception)
overload, the compiler will prefer to use it, e.g.In doing so, we avoid the expensive layer of throw/catch in order to propagate the exception from the awaiter to the method's builder.
A centralized
Exceptional:
section like this would keep the additional code size to a minimum, albeit it would lose some diagnostic information about the location of the error. Alternatively, for more code size, every await could include:New Runtime APIs
ExceptionDispatchInfo.AppendCurrentStackFrame. Just passing the exception from
GetResult(out Exception? exception)
tobuilder.SetException
would result in a diagnostic gap, as the exception would no longer contain data about this async method, data that would normally be populated as part of the throw/catch. Instead, we add a newExceptionDispatchInfo.AppendCurrentStackFrame
that does the minimal work necessary to gather the same data about the current stack frame it would as part of a stack walk, and append relevant data to the exception's stack trace.All of the public awaiters in CoreLib are updated to expose a new
GetResult
overload:TaskAwaiter
TaskAwaiter<TResult>
ValueTaskAwaiter
ValueTaskAwaiter<TResult>
ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter
ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter
ConfiguredValueTaskAwaitable<TResult>.ConfiguredValueTaskAwaiter
And we use default interface method support to add new
GetResult
overloads toIValueTaskSource
andIValueTaskSource<TResult>
:such that these new overloads may be used by the
ValueTask
awaiters. dotnet/runtime's implementations of these interfaces would overrideGetResult(out Exception)
to avoid the default throw/catch.Risks
ExceptionDispatchInfo.AppendCurrentStackFrame
method's overhead to make sure it's meaningfully-enough better than a throw/catch.Alternatives
The simplest alternative is for developers to keep on keeping on as they do today. Regardless of whether this feature exists or not, we still want exceptions to be exceptional, and so developers should continue to try to keep exceptions off the hot path. In situations where a developer detects an expensive use of exception throw/catch due to async, they can manually change the code to avoid throwing via a custom await helper, e.g.
dotnet/runtime#22144 tracks adding new public API for this in dotnet/runtime and will hopefully be addressed in .NET 8, regardless of this issue. This issue ends up being about helping to reduce the situations in which a developer would need to do that and need to know to do that.
Related discussion:
dotnet/csharplang#4450
The text was updated successfully, but these errors were encountered: