8

Not return value optimization in the traditional sense, but I was wondering when you have a situation like this:

private async Task Method1()
{
    await Method2();
}

private async Task Method2()
{
    await Method3();
}

private async Task Method3()
{
    //do something async
}

This could obviously be written more optimally:

private Task Method1()
{
    return Method2();
}

private Task Method2()
{
    return Method3();
}

private async Task Method3()
{
    //do something async
}

I just wondered whether anyone knew if the (MS) compiler was clever enough not to generate state machines for Method1() and Method2() in the first instance?

GazTheDestroyer
  • 20,722
  • 9
  • 70
  • 103
  • 2
    I guess not, since you couldo still call the methods from other places – knittl Mar 21 '14 at 09:26
  • @knittl: Sorry, I'm not sure what you mean. Method1 and 2 still return tasks that can be awaited – GazTheDestroyer Mar 21 '14 at 09:52
  • 3
    Why don't you just try it? Compile the code and check the assembly in reflector or ildasm. Since await is a C# compiler feature (and not a JIT compiler feature) you can easily see if there is a difference. – cremor Mar 21 '14 at 09:59
  • @cremor: Basically because I have never got around to learning CIL. Yes, lazy of me I agree, and probably a good learning exercise. – GazTheDestroyer Mar 21 '14 at 10:23
  • You don't need to know IL to analyze an assembly, there are many tools out there that can translate IL back to C# code. The most popular one is the .NET Reflector (now from RedGate) but there are free alternatives like ILSpy. – cremor Mar 21 '14 at 10:27
  • 1
    I use [JetBrains's dotPeek](http://www.jetbrains.com/decompiler), a free and great tool. It's smart enough to recreate `async/await` statements, but it's also possible to see what the compiler-generated raw state machine looks like. – noseratio Mar 21 '14 at 10:30

2 Answers2

9

No, the C# compiler doesn't optimize it and it should not. These are conceptually two different things, here is a similar question.

IMO, the major difference is in how exceptions are getting propogated into the caller of Method1 and Method2. I demo'ed this behavoir here.

In the first case (without the state machine), an exception will be immediately thrown on the caller's stack frame. If it is unhanded, the app may crash right away (unless there is another async method in the chain of calls on the same stack frame).

In the second case (with the state machine), an exception will remain dormant in the Task object returned to the caller, until it is observed via await task or task.Wait(), some time later. It may get observed on a completely different stack frame, or may not get observed at all. I posted some more details about this here.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    That's a good point that I'd not considered. Any method marked async effectively marshals any exceptions to the returned Task. Thanks! – GazTheDestroyer Mar 21 '14 at 11:08
2

Why do you ask a question you can answer in a minute by simply testing it?

class Program
{
    static void Main(string[] args)
    {
        MainAsync().Wait();
        Console.ReadLine();
    }

    static async Task MainAsync()
    {
        await Method1();
    }

    static async Task Method1()
    {
        await Method2();
    }

    static async Task Method2()
    {
        await Method3();
    }

    static async Task Method3()
    {
        Console.Write("Start");
        await Task.Delay(1000);
        Console.Write("End");
    }
}

This creates four different state machines in IL.

The IL code has to be this way, since you can call the methods from anywhere and they have to behave consistently, so any optimization would have to be done on the JIT level, not the C# compiler. If you don't need await, don't use it - that's your responsibility.

A great example would be method overloads:

static Task Method()
{
  return Method("Default");
}

static async Task Method(string someString)
{
  await SomeThingAsync(someString);
}

It's still just as asynchronous as if you did another await in the parameter-less method - but it avoids a useless state machine.

The only purpose of the async keyword is to allow you to use the await keyword inside a given method. You can still await a method that isn't async - the requirement is returning Task, not having the async keyword.

Using the same example as before, the awaits are superfluous. A simpler way would be this:

class Program
{
    static void Main(string[] args)
    {
        MainAsync().Wait();
        Console.ReadLine();
    }

    static async Task MainAsync()
    {
        await Method1();
        await Method2();
    }

    static Task Method1()
    {
        return Method2();
    }

    static Task Method2()
    {
        return Method3();
    }

    static async Task Method3()
    {
        Console.Write("Start");
        await Task.Delay(1000);
        Console.Write("End");
    }
}
Luaan
  • 62,244
  • 7
  • 97
  • 116