Vaughan Reid's blog

When to consider using ValueTask over Task with async code

As of .NET Core 2.0, you can use ValueTask and ValueTask as return types for your async methods. Its important to understand when and if you should use these instead of a Task and what the trade-offs are if you decide to use it.

I would say that if you aren’t measuring the performance of your application including time spent in garbage collection then do that first before you even consider it. The main reason for ValueTask is when there are times that your async code has synchronous paths which return a value immediately. Previously, in those cases you were still paying the allocation cost of adding a new Task in the heap even though you weren’t using the state machine to wait for async completions. ValueTask is designed for these cases where returning the value immediately is the hot path of the method.

A good example is a method that checks a cache, if the value is not in the cache then it will make an expensive async call and save the result in the cache for the next time. After the first call, all other calls just return the cached value as long as the cache hasn’t expired. In these cases you may want to investigate using ValueTask to save time your application would spend in garbage collection. eg: Low latency and high throughput.

So the question is, why is ValueTask better in these cases. Its actually in the name. ValueTask is a struct (value type) and so it won’t be allocated on the heap in the cases that there is an immediate value. Its really important to note that when there is any asynchronous work, then it will be allocated on the heap since it would be converted to a Task. ValueTask is actually a little more expensive when you need to allocate to the heap and its also slightly slower when running the code since you are passing a value object instead of the reference to an object like you would with reference types.

To understand the difference, I did a benchmark with BenchmarkDotNet to measure the difference between using ValueTask and Task. Its always good to get real numbers when you are talking about performance.

I created a Person class that will either be returned directly or after an async yield. I have 4 benchmarks against aysnc/immediate and Task/ValueTask to compare the different scenarios.


public class Person
{
	public Person(string name, int age)
	{
		Name = name;
		Age = age;
	}
	
	public string Name { get; private set; }
	
	public int Age { get; private set; }
}

public class TaskTests
{
	Person _person;

	public TaskTests()
	{
		_person = new Person("Joe", 100);
	}

	public async Task<Person> TaskResponse(bool sync)
	{
		if (sync)
		{
			return _person;
		}
		else
		{
			await Task.Yield();
			return _person;
		}
	}

	public async ValueTask<Person> ValueTaskResponse(bool sync)
	{
		if (sync)
		{
			return _person;
		}
		else
		{
			await Task.Yield();
			return _person;
		}
	}

}

[MemoryDiagnoser]
public class TaskBenchmarks
{
	private TaskTests taskTests = new TaskTests();

	[Benchmark]
	public async Task Task()
	{
		var result = await taskTests.TaskResponse(true);
	}

	[Benchmark]
	public async Task TaskWithAwait()
	{
		var result = await taskTests.TaskResponse(false);
	}

	[Benchmark]
	public async ValueTask ValueTask()
	{
		var result = await taskTests.ValueTaskResponse(true);
	}

	[Benchmark]
	public async ValueTask ValueTaskWithAwait()
	{
		var result = await taskTests.ValueTaskResponse(false);
	}

}

Below are the final results:

ValueTaskBenchmark

What is interesting is that in pure execution time, ValueTask is slightly slower. The win is the fact that the ValueTask call that returns immediately doesn’t allocate anything. Of course this is only good if this is the hot path of your method. You can see that it will save 15 collections for every 1000 calls. In some applications that will make a difference.

If you decide to make this change, please note that there are some breaking changes in implementation between a Task and a ValueTask which you need to be aware of.

You cannot await the ValueTask more than once since the underlying object may have been recycled already and in use by another operation.

ValueTask<int> vt = ValueTaskResponse();
int a = await vt;
int b = await vt;

You cannot await the same task concurrently for the same reason as above.


ValueTask<int> vt = ValueTaskResponse();
Task.Run(async () => await vt);
Task.Run(async () => await vt);

You cannot call GetAwaiter().GetResult() to block until the operation completes. You should only ever call that if you are sure that the ValueTask has actually completed.


ValueTask<int> vt = ValueTaskResponse();
int result = vt.GetAwaiter().GetResult();

For most modern applications, these patterns shouldn’t be that common but it is really important to understand the implications of the changes you make when you are looking at performance gains.