-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
await ValueTask performance #11803
Comments
The root cause here is likely https://github.com/dotnet/coreclr/issues/18542. But even without that, there are still additional costs to ValueTask as discussed in https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/, and the only reason for using the non-generic ValueTask is if you believe you'll be able to avoid asynchronous allocations via IValueTaskSource… otherwise, you're better off just returning Task and using CompletedTask (which is why the non-generic ValueTask was only introduced when the IValueTaskSource mechanism was introduced). |
#10540 more heavily affects ConfigureAwait Tweaking to use the [Benchmark(OperationsPerInvoke = LoopIterations)]
public async ValueTask ValueTask()
{
for (int i = 0; i < LoopIterations; i++) And adding
The issue with the test is Changing to [Benchmark(OperationsPerInvoke = LoopIterations)]
public async ValueTask ValueTaskIfIsCompletedSuccessfully()
{
for (int i = 0; i < LoopIterations; i++)
{
var valueTask = new ValueTask();
if (!valueTask.IsCompletedSuccessfully)
await valueTask;
else
valueTask.GetAwaiter().GetResult();
}
} Then you get
|
Good catch, yes. While that doesn't matter specifically for |
Aside, optimizing with Though you'd like need to be quite sensitive to the performance impact to make the extra code completely worthwhile:
For const int LoopIterations = 100;
[MethodImpl(MethodImplOptions.NoInlining)]
static Task OperationAsync() => Task.CompletedTask;
[MethodImpl(MethodImplOptions.NoInlining)]
static ValueTask ValueTaskOperationAsync() => default;
[Benchmark(OperationsPerInvoke = LoopIterations)]
public async ValueTask ValueTask()
{
for (int i = 0; i < LoopIterations; i++)
{
ValueTask task = ValueTaskOperationAsync();
await task;
}
}
[Benchmark(OperationsPerInvoke = LoopIterations)]
public ValueTask ValueTaskSyncFastPath()
{
for (int i = 0; i < LoopIterations; i++)
{
var valueTask = ValueTaskOperationAsync();
if (!valueTask.IsCompletedSuccessfully)
{
return ValueTaskAsync(i, valueTask);
}
else
{
valueTask.GetAwaiter().GetResult();
}
}
return default;
async ValueTask ValueTaskAsync(int iter, ValueTask valueTask)
{
await valueTask;
iter++;
for (int i = iter; i < LoopIterations; i++)
{
valueTask = ValueTaskOperationAsync();
await valueTask;
}
}
}
[Benchmark(OperationsPerInvoke = LoopIterations, Baseline = true)]
public async Task CompletedTask()
{
for (int i = 0; i < LoopIterations; i++)
{
Task task = OperationAsync();
await task;
}
}
[Benchmark(OperationsPerInvoke = LoopIterations)]
public Task CompletedTaskSyncFastPath()
{
for (int i = 0; i < LoopIterations; i++)
{
Task task = OperationAsync();
if (!task.IsCompletedSuccessfully)
{
return CompletedTaskAsync(i, task);
}
}
return Task.CompletedTask;
async Task CompletedTaskAsync(int iter, Task task)
{
await task;
iter++;
for (int i = iter; i < LoopIterations; i++)
{
task = OperationAsync();
await task;
}
}
} |
Saying that the loop amortizes the
[MethodImpl(MethodImplOptions.NoInlining)]
static Task OperationAsync() => Task.CompletedTask;
[MethodImpl(MethodImplOptions.NoInlining)]
static ValueTask ValueTaskOperationAsync() => default;
[Benchmark]
public async ValueTask ValueTask()
{
await ValueTaskOperationAsync();
}
[Benchmark]
public ValueTask ValueTaskSyncFastPath()
{
var valueTask = ValueTaskOperationAsync();
if (!valueTask.IsCompletedSuccessfully)
{
return ValueTaskAsync(valueTask);
}
else
{
valueTask.GetAwaiter().GetResult();
}
return default;
async ValueTask ValueTaskAsync(ValueTask vt)
{
await vt;
}
}
[Benchmark(Baseline = true)]
public async Task CompletedTask()
{
await OperationAsync();
}
[Benchmark]
public Task CompletedTaskSyncFastPath()
{
Task task = OperationAsync();
if (!task.IsCompletedSuccessfully)
{
return CompletedTaskAsync(task);
}
return Task.CompletedTask;
async Task CompletedTaskAsync(Task t)
{
await t;
}
} The normal optimization that applies here is pass-though rather than Task DoAsync()
{
return OperationAsync();
} rather than async Task DoAsync()
{
await OperationAsync();
} Though you have to |
Note: related https://github.com/dotnet/coreclr/issues/21973 |
We will continue to look for ways to reduce the overheads, but I don't think there's anything actionable here anymore, so closing. Thanks. |
I did some performance testing for async-await on a completed
ValueTask
and compared it to aTask.CompletedTask
. I see that forTask.CompletedTask
it does not matter if I bypassawait
if I check theTask
forIsCompletedSuccessfully
, but forValueTask
it has significant performance impact (3x). Is there a missing optimization in the async code generated forasync
-await
when usingValueTask
?The text was updated successfully, but these errors were encountered: