Does async/await have an extra cost associated when returning early from the method?

Viewed 506

Does async/await have an extra cost associated when returning early from the method compared to the synchronous method?

Take these examples:

public async Task<bool> TryDoSomethingAsync()
{
    if ( Is99PercentOfTheCases() )
        return false;  // does this path...

    await DoSomethingAsync();

    return true;
}

// versus 

public bool TryDoSomething()
{
    if ( Is99PercentOfTheCases() )
        return false; // ...have the same cost as this path?

    DoSomething();

    return true;
}

I know async/await has an extra cost associated and therefore you need to be careful around tight loops - see i.e. https://www.red-gate.com/simple-talk/dotnet/net-framework/the-overhead-of-asyncawait-in-net-4-5/

But when is this cost occurring?

  • Is it when you mark a method as async?
  • Is it when it needs to await?
  • Or is it related to the Task return type?
  • Especially: does the performance hit still occur when returning early for an async method?

In the end it's always best to profile specific cases, but I'm interested in the theory behind it.

1 Answers

The code in your TryDoSomethingAsync get converted by the compiler to IL code that is, more or less, the equivalent of this:

public Task<bool> TryDoSomethingAsync()
{
    TryDoSomethingAsyncStateMachine stateMachine = new TryDoSomethingAsyncStateMachine();
    stateMachine._this = this;
    stateMachine._builder = AsyncTaskMethodBuilder<bool>.Create();
    stateMachine._state = -1;
    AsyncTaskMethodBuilder<bool> _builder = stateMachine._builder;
    _builder.Start(ref stateMachine);
    return stateMachine._builder.Task;
}

private sealed class TryDoSomethingAsyncStateMachine : IAsyncStateMachine
{
    public int _state;
    public AsyncTaskMethodBuilder<bool> _builder;
    public UserQuery _this;

    private TaskAwaiter _awaiter;

    private void MoveNext()
    {
        int num = _state;
        bool result;
        try
        {
            TaskAwaiter awaiter;
            if (num == 0)
            {
                awaiter = _awaiter;
                _awaiter = default(TaskAwaiter);
                num = (_state = -1);
                goto IL_0080;
            }
            if (!_this.Is99PercentOfTheCases())
            {
                awaiter = _this.DoSomethingAsync().GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (_state = 0);
                    _awaiter = awaiter;
                    TryDoSomethingAsyncStateMachine stateMachine = this;
                    _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
                goto IL_0080;
            }
            result = false;
            goto end_IL_0007;
        IL_0080:
            awaiter.GetResult();
            result = true;
        end_IL_0007:;
        }
        catch (Exception exception)
        {
            _state = -2;
            _builder.SetException(exception);
            return;
        }
        _state = -2;
        _builder.SetResult(result);
    }

    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

There is considerable more code that is run that the plain async method.

However, the code that runs the Is99PercentOfTheCases() is still fairly light. It will be quick, but not as quick as the non-async method.

Related