ValueTask vs. Task logo

ValueTask vs. Task

Karol Rossa
3 min readJun 19, 2022

We have already covered the asynchronous paradigm and performance comparison of async vs. sync API calls.
Today I will show you how to save memory using ValueTask. In a previous story, we talked about Garbage Collector and how it can slow down the application during memory collection.

ValueTask should be used in an asynchronous method that can execute synchronously. What the hell is that? The simplest example I will use is a method that will most likely return value from cache and, if necessary, make an async call to an external resource. In layman’s terms, if your async method can return without reaching the await statement, use ValueTask as the return type.
If you are interested in why ValueTask is more efficient and to find the hidden cost of using Task, keep reading till the end :)

Code Repository

https://github.com/krossa/ValueTaskBenchamrk

As you can see, the only difference between GetDataWithValueTaskAsync and GetDataWithTaskAsync is the return type. Respectively ValueTask vs. Task.

For benchmark, I will use benchmarkdotnet.org with two attributes

  • [Benchmark] — marks method for evaluation
  • [MemoryDiagnoser] — adds information about memory allocation
https://github.com/krossa/ValueTaskBenchamrk

It is a simple class that runs each method from a repository.

Results

ValueTask vs. Task benchamrk
ValueTask vs. Task benchamrk

Execution time is nearly the same for each case. What matters is memory allocation. Only the Task method has caused memory allocation on the heap. Value from the benchmark represents allocation per single call. So if the Task method is called multiple times, it can significantly increase memory allocation, which has to be released by Garbage Collector. Additionally, each call creates a separate Task object that needs to be handled individually by GC. This happens because Task is a reference type that always gets allocated on the heap, which is the hidden cost of using Task over ValueTask.

So what is a ValueTask, and why is there no memory allocation in our example?

ValueTask is a value type struct that wraps a Task<TResult> and a TResult, only one of which is used. As we know, struct, by default, gets allocated on the stack, not the heap. Stack allocation is cheaper because it gets released automatically, not by GC.

Under the hood, it is a discriminated union and can return one of two things. Either a plain value TResult (value taken from cache) or a Task<TResult> (returned by async GetData). In our example, a task must be created every 1 hour when the cache expires. Every other time method returns a plain TResult value without any excessive memory allocation

There are also drawbacks to using ValueTask, so you must be careful when deciding what to use. There are a couple of scenarios when ValueTask can’t be used:

  • Awaiting the instance multiple times.
  • Calling AsTask multiple times.
  • Using .Result or .GetAwaiter().GetResult() when the operation hasn’t yet completed, or using them multiple times.

--

--