5

I have an async method that looks approximately like this:

async Task<int> ParseStream()
{
    var a = await reader.ReadInt32();
    var b = await reader.ReadInt32();
    return a + b;
}

This method is going to work synchronously most of the time as data is already ready. So looks like good idea to replace return type with ValueTask to reduce allocations. But this calls reader.ReadInt32() return Task.

So the question is: is there any sense to return ValueTask from method that internally awaits some Tasks?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
wiuwiu
  • 63
  • 1
  • 5
  • I assume `reader` and `stream` are meant to be related/the same? If so, how do you know "most of the time as data is already ready". If I'm handed an arbitrary `Stream`, I cannot assume much about how much data is readily available, so it seems there are some assumptions you're able to make that you've not mentioned in the question? – Damien_The_Unbeliever Jan 28 '20 at 10:55
  • Oh, I didn't notice this stream - it's not important for this case. This Parse method is usually called when response is already here. – wiuwiu Jan 28 '20 at 10:58
  • 2
    This is a bit like asking "does it make any sense for me to avoid memory allocations if the methods I call allocate memory"? (Just as an analogy; conserving memory is not the primary goal of `ValueTask`.) Whether or not to use `ValueTask` should depend on whether it makes sense for *your* scenario to use it (as you restrict callers in how they can await your results), less than on what your implementation is currently relying on. It makes little sense to (say) wait for a new implementation that does use `ValueTask` for `ReadInt32` and only then switch your method as well. – Jeroen Mostert Jan 28 '20 at 11:01
  • Are you asking if there are advantages at returning `ValueTask` instead of `Task`? – Theodor Zoulias Jan 28 '20 at 11:06
  • Yes. For example, let's imagine that in some situation this calls `reader.ReadInt32()` would run synchronously, and I would replace return type with the ValueTask. Would this method allocate in such optimistic scenario? – wiuwiu Jan 28 '20 at 11:09
  • Or this memory-optimization have some notable effect only when `reader.ReadInt32()` calls return ValueTask? – wiuwiu Jan 28 '20 at 11:16

3 Answers3

4

Changing the signature of your method to return a ValueTask instead of Task:

async ValueTask<int> ParseStreamAsync()
{
    var a = await reader.ReadInt32Async();
    var b = await reader.ReadInt32Async();
    return a + b;
}

...has the advantage that callers invoking your method will avoid an object allocation in case that both calls to reader.ReadInt32Async will complete synchronously. But this may not be a great advantage because the two calls to reader.ReadInt32Async may still allocate a Task object each, depending on how this method is implemented. It is theoretically possible that some common Task<Int32> return values will be cached, but in practice not very probable. It would be different if the return value was Task<bool>, were caching the only two possible values would be cheap. Caching Task<TResult> objects was the only way available for reducing allocations before the introduction of ValueTasks.

So by using ValueTask instead of Task you can reasonably expect that you'll reduce the object allocations from 3 to 2 on each invocation, which is not very impressive, but not negligible either.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
2

If you are not sure if it is a good use for ValueTask<T> it's probably because it isn't.

Understanding the Whys, Whats, and Whens of ValueTask

You can cache and await a Task<T> as many times as you want. It only has the downside of an heap allocation.

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
0

Yes, because it is one part of pattern. This what you show is no blocking use of async pattern, so if you don't use task in return it must be blocking.

Leszek Mazur
  • 2,443
  • 1
  • 14
  • 28