5

I'm doing a benchmark on Task<T> and ValueTask<T>. Source code below:

#LINQPad optimize+     // Enable compiler optimizations

void Main()
{
    Util.AutoScrollResults = true;
    BenchmarkRunner.Run<TaskAndValueTaskComparsion>();
}

[ShortRunJob]
public class TaskAndValueTaskComparsion
{
    [Benchmark]
    public ValueTask<int> RunValueTaskWithNew()
    {
        return new ValueTask<int>(1);
    }
    
    [Benchmark]
    public Task<int> RunTaskFromResult()
    {
        return Task.FromResult(1);
    }
}

enter image description here

For the result, the Task<T> is way faster than ValueTask<T>. But why? I really expected the ValueTask<T> would cause less allocation when returning task objects.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Will Huang
  • 2,955
  • 2
  • 37
  • 90
  • 7
    I assume it's because `Task.FromResult(1)` is cached and the same reference is always being returned from your method – haim770 May 30 '22 at 09:46
  • 3
    `Task.FromResult()` will cache certain commonly used values, such as `true`, `false`, `0`, `1` etc. Here's some links to the implementation: https://github.com/dotnet/runtime/blob/486b4d1a36aef5bbe6a77bb2c3412772e712961e/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L5219-L5272 https://github.com/dotnet/runtime/blob/486b4d1a36aef5bbe6a77bb2c3412772e712961e/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskCache.cs#L10 – Martin Costello May 30 '22 at 09:53
  • 5
    Your code is explicitly allocating a new ValueTask every time. It's not even the same API. Use `ValueTask.FromResult` at least. – Panagiotis Kanavos May 30 '22 at 09:53
  • 1
    [Task.FromResult](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L5219) caches tasks when possible, especially for value types – Panagiotis Kanavos May 30 '22 at 09:54
  • To the OP: could you redo the same benchmark, with a different value for the tasks instead of `1`? For example with the value `42`. To the commentators: please avoid answering questions in the comments! – Theodor Zoulias May 30 '22 at 10:13
  • 3
    I tried `65534` as the value. The `ValueTask` truly faster a bit. New result here: https://user-images.githubusercontent.com/88981/170971437-cb2d33f9-8211-42d8-b619-0433af649c36.png – Will Huang May 30 '22 at 10:16
  • 1
    You've also failed to include information on what framework/JIT compiler is used. This matters a great deal in terms of optimization. And, of course, this being a microbenchmark, this alone really says very little -- the real benefits of `ValueTask` are not apparent until you benchmark code that (normally) allocates very many `Task`s for very small, granular work, and look at memory use. The time needed for a method call is not unimportant, but a lot less interesting. See https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/ . – Jeroen Mostert May 30 '22 at 10:41

1 Answers1

5

Thanks for all the commenters. Let's summarize all these valuable info.

There is a TaskCache.cs in the .NET runtime that create cacheable Task for various primitive type's value such as true(bool), false(bool), -1 ~ 9(int). The Task.FromResult leverage these cached Task for it.

So, the benchmark result is incorrect because the Task.FromResult is always return cached version of Task object, and the new ValueTask<int>(1) always returns a new value.

For more information, please read Understanding the Whys, Whats, and Whens of ValueTask.

If you can read Chinese, I also wrote a post yesterday. See here.

Will Huang
  • 2,955
  • 2
  • 37
  • 90
  • *"the `new ValueTask(1)` always return a new object instance."* -- Value types are [rarely](https://stackoverflow.com/questions/13049/whats-the-difference-between-struct-and-class-in-net "What's the difference between struct and class in .NET?") characterized as "objects". This characterization is generally [reserved](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/structs#151-general) for reference types. – Theodor Zoulias May 31 '22 at 08:39
  • @TheodorZoulias What kind of "term" I can use instead of "object"? – Will Huang May 31 '22 at 09:31
  • @TheodorZoulias I refined my sentence. Just simply remove "object" from it. Thanks for your comment. – Will Huang May 31 '22 at 09:33
  • 1
    My preference would be "*always returns a new value.*" -- AFAIK the term "instance" is also associated with reference types. For example `int i = 1` people generally don't say that they created a new `int` instance. – Theodor Zoulias May 31 '22 at 09:44