In this article, we will take a look at ValueTask structure. As a value type, it comes with all the problems that value types in general have. So, we can reap the benefits of this structure, but we have to be cautious.
In the previous articles, we explored various aspects of asynchronous programming in .NET. We talked about the motivation behind this way of programming and about some general pitfalls and guidelines. Apart from that, we explored Task-Based Asynchronous Pattern. In that article, we saw how we can use Task
and Task
class for making our code asynchronous and reap the benefits of parallel execution of the code. However, using Task
or Task
class has one performance bottleneck that we haven’t mentioned in the previous articles.
In a nutshell, these classes cause the unnecessary allocation when the result is immediately available. This means, that even if a result is already available, a new object of Task
or Task
will always be created. Now, we mentioned that the async
/await
concept that we used in the previous articles has been around since 4.5 version of .NET. This feature has been enhanced since C# 7, i.e., .NET 4.7 version with ValueTask
structure, that can be the return of the async
functions.
ValueTask Structure
ValueTask
structure first appeared on corefxlab repository back in 2015. This repository is used for experimentation and exploring new ideas that may or may not make it into the main corefx repository. Corefx repository is the repository where all .NET Core fundamental libraries are. It was developed and suggested by the Stephen Taub for System.Threading.Tasks.Channels
library. Back then, Stephen provided a short explanation of it:
ValueTask
is a discriminated union of a T
and a Task
, making it allocation-free for ReadAsync
to synchronously return a T
value it has available (in contrast to using Task.FromResult
, which needs to allocate a Task
instance). ValueTask
is awaitable, so most consumption of instances will be indistinguishable from with a Task
.
A lot of people saw benefits of using this structure and it was included in C#7 as a part of System.Threading.Tasks.Extensions
NuGet package. So, before we dive into the ValueTask
structure, let’s examine the problem it is used to solve. Since Task (Task)
is a reference type, returning Task
object from async
method means allocating it on heap every time. And this is needed in many cases.
However, there are cases where the async
method returns a cached result or completes synchronously. In these cases, this allocation is unnecessary and can become costly in performance critical parts of the code. Up to .NET 4.7 version, there was no way to avoid this because an async
method had to return Task
, Task<T>
, or void
(the last one is generally frowned upon). In this version of .NET, this was extended, meaning async
methods could return any type as long as it has GetAwaiter
method accessible. ValueTask
is a concrete example of such type, that was added to this version too.
You can explore corefx repository and take a look at the complete implementation of ValueTask,
but here is the part of the API that we are interested in:
public ValueTask(TResult result);
public ValueTask(Task<TResult> task);
public static implicit operator ValueTask<TResult>(Task<TResult> task);
public static implicit operator ValueTask<TResult>(TResult result);
public Task<TResult> AsTask();
public bool IsCompletedSuccessfully { get; }
public TResult Result { get; }
public ValueTaskAwaiter GetAwaiter();
public ValueTaskAwaiter ConfigureAwait(bool continueOnCapturedContext);
Being a structure, ValueTask
enables writing async
methods that do not allocate memory when running synchronously. API consistency of async/await
concept has not been compromised this way. Apart from that, this structure can be awaited by itself which makes it easy to use. For example, if we run this simple code:
public void Example()
{
int res = MultipyAsync(0, 0).Result;
Console.WriteLine(res);
}
private async Task<int> MultipyAsync(int x, int y)
{
if (x == 0 || y == 0)
{
return 0;
}
return await Task.Run(() => x * y);
}
In MultiplyAsync
method, we are simulating the case where we want to avoid the use of the Task
and just return a simple integer. That is done in the if
statement of the method, where we are basically checking if either of the passed arguments is zero. The problem is that the above code will create a Task
object even if the condition in the if
statement is true
. We solve this problem like this:
public void Example()
{
int res = MultiplyAsync(0, 0).Result;
Console.WriteLine(res);
}
private async ValueTask<int> MultiplyAsync(int x, int y)
{
if (x == 0 || y == 0)
{
return 0;
}
return await Task.Run(() => x * y);
}
Benefits and Tradeoffs
As mentioned previously, there are two main benefits of using ValueTask
:
- Performance improvements
- Increased implementation flexibility
So, what are the numbers behind the performance improvements? Observe this code:
public async Task<int> TestTask(int delay)
{
await Task.Delay(delay);
return 1;
}
If we run this code, it will take 120ns with JIT to be executed. Now, if we replace Task
with ValueTask
like this:
public async ValueTask<int> TestValueTask(int delay)
{
await Task.Delay(delay);
return 1;
}
we would get the execution time of 65ns with JIT. Now, it is true that because of the Task.Delay
, we don’t have the synchronous execution, still, we see improvements in the execution time.
Another benefit that we mentioned is increased implementation flexibility. Now, what exactly does this mean? Well, implementations of an async
interface that should be synchronous would be forced to use either Task.Run
or Task.FromResult
. This would, of course, result in performance issues that we discussed previously. When we use ValueTask
, we have more possibility to choose between synchronous or asynchronous implementation. Keep in mind that this may indicate that if you have this case, your code might not be designed well.
For example, observe this interface:
public interface IThing<T>
{
ValueTask<T> DoTheThingAsync();
}
And let’s say that you want to call it from the code like this:
IThing<T> thing = thingFactory.getThing();
var x = await thing.DoTheThingAsync();
Because we used ValueTask
in the interface, implementation of that interface can be either synchronous or asynchronous. We can reap this benefit and basically skip adding some functions into IThing
that would handle synchronous behaviors. Using this interface is much easier this way. Here is a synchronous implementation of the interface above:
public class SynchronousThing<T> : IThing<T>
{
public ValueTask<T> DoTheThingAsync()
{
var value = default(T);
return new ValueTask<T>(value);
}
}
And here is an asynchronous implementation of the same interface:
public class AsynchronousThing<T> : IThing<T>
{
public ValueTask<T> DoTheThingAsync()
{
var value = default(T);
await Task.Delay(100);
return new ValueTask<T>(value);
}
}
However, there are some tradeoffs that we must take into consideration before using ValueTask
. It is easy to think that using of ValueTask
instead of Task
should be done by default, which is of course not the case. For example, even though ValueTask
helps us avoid unnecessary allocation in the cases when a result is available synchronously, it also contains two fields.
It is important to remember that this is a structure we use here, meaning we use value type and all its burdens. The Task
on the other hand, is a reference type with just one field. When you use ValueTask
, we have more data to handle and take care of. If such method is awaited within an async
method, the state machine for that async
method will be larger as well, because storing the whole structure, in general, requires more space than storing single reference.
This is why guys from Microsoft are actually suggesting to use Task
or Task
as default return type form async
methods. Only after performance analysis, one should consider using ValueTask
instead.
Conclusion
ValueTask
is a structure that was introduced in .NET 4.7 and that gives us quite a few possibilities when working with asynchronous methods within .NET. However, it doesn’t come without a price. It is quite good for performance critical methods that are executed synchronously. Using them, we avoid allocation of the unnecessary objects. Still, as a value type, it comes with all the problems that value types in general have. So, we can reap benefits of this structure, but we have to be cautious.
History
- 11th June, 2018: Initial version