0

Can anyone explain why awaiting a ValueTask method has about 4x overhead compared to awaiting a Task method in this micro-benchmark?

It's allocating less, as expected, but the bigger overhead is unexpected.

I thought awaiting a ValueTask would be faster or same as awaiting a Task method.

|         Method |     Mean |     Error |    StdDev |      Min |      Max |   Median | Allocated |
|--------------- |---------:|----------:|----------:|---------:|---------:|---------:|----------:|
|      AsyncTask | 1.905 ms | 0.5159 ms | 0.0283 ms | 1.881 ms | 1.936 ms | 1.898 ms |      73 B |
| AsyncValueTask | 8.089 ms | 1.1259 ms | 0.0617 ms | 8.018 ms | 8.132 ms | 8.117 ms |       9 B |
[ShortRunJob]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
[MemoryDiagnoser]
[MarkdownExporter]
public class AsyncMethodCallsBench
{    
    [Benchmark]
    public async Task<int> AsyncTask()
    {
        var sum = 0;
        for (int i = 0; i < 1_000_000; i++)
        {
            sum += await Foo.AsyncTaskMethod(1);
        }
        return sum;
    }
    
    [Benchmark]
    public async ValueTask<int> AsyncValueTask()
    {
        var sum = 0;
        for (int i = 0; i < 1_000_000; i++)
        {
            sum += await Foo.AsyncValueTaskMethod(1);
        }
        return sum;
    }
}

public class Foo
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static Task<int> AsyncTaskMethod(int arg) => Task.FromResult<int>(arg * 2);

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static ValueTask<int> AsyncValueTaskMethod(int arg) => ValueTask.FromResult<int>(arg * 2);
}

EDIT: Apparently Task.FromResult() uses cache for some primitive values. If I pass i instead of 1 for an argument, I get following results:

|         Method |     Mean |     Error |    StdDev |      Min |      Max |   Median |      Gen0 |  Allocated |
|--------------- |---------:|----------:|----------:|---------:|---------:|---------:|----------:|-----------:|
|      AsyncTask | 3.736 ms | 0.2676 ms | 0.0147 ms | 3.721 ms | 3.750 ms | 3.736 ms | 4300.7813 | 71999714 B |
| AsyncValueTask | 8.151 ms | 1.0713 ms | 0.0587 ms | 8.108 ms | 8.218 ms | 8.126 ms |         - |        9 B |

Task version now allocates significantly more, but is still 2x faster than ValueTask method.

zigzag
  • 579
  • 5
  • 17
  • if you dont use the value returned by a ValueTask, this will happen, try benchmarking with that factor. in your bench you are still returning ValueTask, your functions should be void and only consume some data from a Task function and a ValueTask function. then see that happens. – AliSalehi Jan 23 '23 at 23:19
  • @GSerg I rerun benchmark with `i` as an argument, which probably avoids most of the caching. Somehow, `Task` is still 2x faster. – zigzag Jan 23 '23 at 23:56

0 Answers0