7

According to the documentation a ValueTask<TResult>...

Provides a value type that wraps a Task<TResult> and a TResult, only one of which is used.

My question is about the state machine that the C# compiler generates when the async keyword is encountered. Is it smart enough to generate a ValueTask<TResult> that wraps a TResult, when the result is available immediately, or one that wraps a Task<TResult>, when the result comes after an await? Here is an example:

static async ValueTask<DateTime> GetNowAsync(bool withDelay)
{
    if (withDelay) await Task.Delay(1000);
    return DateTime.Now;
}

static void Test()
{
    var t1 = GetNowAsync(false);
    var t2 = GetNowAsync(true);
}

Calling GetNowAsync(false) should return a TResult wrapper, because nothing is awaited, and calling GetNowAsync(true) should return a Task<TResult> wrapper, because a Task.Delay is awaited before the result becomes available. I am worried about the possibility that the state machine always returns Task wrappers, nullifying all the advantages of the ValueTask type over the Task (and keeping all the disadvantages). As far as I can tell the properties of the type ValueTask<TResult> offer no indication about what it wraps internally. I pasted the code above to sharplab.io, but the output didn't help me to answer this question either.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Compiler doesn't change async method signature, so if it returns `ValueTask` in your code it will keep returning it after state machine generation. There is no room for it to be "smart enough". – Dmytro Mukalov Aug 10 '19 at 07:28
  • @DmytroMukalov you are right. My question is about what is wrapped inside the returned `ValueTask` in each case. – Theodor Zoulias Aug 10 '19 at 07:40
  • A `Task` type won't leak from the `Task.Delay` if you're asking about that, the `ValueTask` will always be returned. Each async method is represented by its own state machine which exposes specific awaiter type will be used by higher level consumers and the fact the `Task.Delay` exposes `Task` has no influence on a state machine around `GetNowAsync` - all it "knows" is how to get `Task` awaiter and how to use an awaiter to get a result asynchronously. – Dmytro Mukalov Aug 10 '19 at 08:09
  • @DmytroMukalov I don't think you have understood my question. Are you familiar with `ValueTask`s? If not, here is a nice introduction: [Understanding the Whys, Whats, and Whens of ValueTask](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/). The type that is returned by `Task.Delay` has no implications to my question. I am asking about the implications of `await`, that could (or could not) occur during a call to an `async` method. – Theodor Zoulias Aug 10 '19 at 08:30
  • My answer point was about that any await encountered inside an async method is internal part of its state machine and it has no influence on the public part of the state machine, that is an awaiter it returns. In other words the fact the await may or may not occurs has implications only on a way of getting result inside your state machine but result will be always exposed by its public awaiter in your case `ValueTask`. – Dmytro Mukalov Aug 10 '19 at 08:43
  • @DmytroMukalov I understand what you say and I agree. My question has to do with the non-public part of the `ValueTask` values that are returned by the state machine, specifically with what is wrapped inside them, because it has performance implications. My benchmarks show that a `ValueTask` that wraps a `TResult` is created+awaited 3 times faster than a `ValueTask` that wraps a `Task`. Btw this gave me an idea about how to answer my question! – Theodor Zoulias Aug 10 '19 at 08:51

2 Answers2

3

I guess I should answer my own question, since I know the answer now. The answer is that my worries were unwarranted: the C# compiler is smart enough to emit the right type of ValueTask<TResult> in every case. It emits a value-wrapper when the result is synchronously available, and a task-wrapper when it isn't.

I came to this conclusion by performance measurements: by measuring the memory allocated in each case, and the time needed to create the same amount of tasks. The results are clear and consistent. For example a ValueTask<int> consumes exactly 12 bytes when it wraps an int value, and exactly 48 bytes when it wraps a Task<int>, so there is no doubt about what's going on under the hoods.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    I'm glad to hear that this is the answer. If understanding the inner workings of `ValueTask` were actually necessary to use Linq.Async effectively, then the whole API would be deeply flawed. – StackOverthrow Aug 23 '19 at 16:25
-1

The compiler is dumb enough to do what it's told:

https://source.dot.net/#System.Private.CoreLib/shared/System/Threading/Tasks/ValueTask.cs,409

[AsyncMethodBuilder(typeof(AsyncValueTaskMethodBuilder<>))]
[StructLayout(LayoutKind.Auto)]
public readonly struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>

Beware of using ValueTask:

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • Btw "beware" is not the right way to approach this type. The `ValueTask` is going to be used heavily by core libraries like [Linq.Async](https://github.com/dotnet/reactive/tree/master/Ix.NET/Source/System.Linq.Async/System/Linq/Operators) ([Supporting IAsyncEnumerable with LINQ](https://www.youtube.com/watch?v=Ktl8K2b1-WU&t=938)), so understanding it is crucial. – Theodor Zoulias Aug 11 '19 at 04:09
  • @TheodorZoulias, if you're using `async foreach` you're not really directly interacting with `ValueTask`. But if you're directly interacting with `IAsyncEnumerable`, beware of `ValueTask`. – Paulo Morgado Aug 11 '19 at 13:50
  • I don't need to beware of the tools I understand, but of the tools I don't. This is the point of this question, to understand the tool. – Theodor Zoulias Aug 11 '19 at 15:08
  • What's missing, then? – Paulo Morgado Aug 11 '19 at 19:05
  • 1
    Is missing an answer to my question. I now know it, and I may post it if no one else does. – Theodor Zoulias Aug 12 '19 at 05:25