Skip to content

async Local function use with locals allocate (even if never called) #18946

@benaadams

Description

@benaadams

Version Used:

.NET Command Line Tools (2.0.0-preview1-005805)

Product Information:
 Version:            2.0.0-preview1-005805
 Commit SHA-1 hash:  78868ad33a

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.16179
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.0.0-preview1-005805\

Microsoft .NET Core Shared Framework Host

  Version  : 2.0.0-preview1-002021-00
  Build    : e9456614cfe65b6dc7c5326a597337159142c84c

Steps to Reproduce:
https://github.com/benaadams/Issues/tree/master/LocalFunctionAllocations

Pass via params: No allocations

public ValueTask<int> GetTotalQuantityAsync()
{
    {
        int total = 0;
        for (int i = 0; i < Orders.Count; i++)
        {
            var task = Orders[i].GetOrderQuantityAsync();
            if (!task.IsCompletedSuccessfully) return Awaited(task, total, i);
            total += task.Result;
        }
        return new ValueTask<int>(total);
    }

    async ValueTask<int> Awaited(ValueTask<int> task, int total, int i)
    {
        total += await task;
        for (i++; i < Orders.Count; i++)
        {
            task = Orders[i].GetOrderQuantityAsync();
            total += (task.IsCompletedSuccessfully) ? task.Result : await task;
        }
        return total;
    }
}

Pass via locals: Allocations (even when Local function is never called)

public ValueTask<int> GetTotalQuantityLocalsAsync()
{
    int total = 0;
    int i = 0;
    ValueTask<int> task;
    for (; i < Orders.Count; i++)
    {
        task = Orders[i].GetOrderQuantityLocalsAsync();
        if (!task.IsCompletedSuccessfully) return Awaited();
        total += task.Result;
    }
    return new ValueTask<int>(total);

    async ValueTask<int> Awaited()
    {
        total += await task;
        for (i++; i < Orders.Count; i++)
        {
            task = Orders[i].GetOrderQuantityLocalsAsync();
            total += (task.IsCompletedSuccessfully) ? task.Result : await task;
        }
        return total;
    }
}

Expected Behavior:
No allocations

Actual Behavior:
2.45 kB of allocations (and performance impact)

                               Method |          Mean |     StdDev |       Op/s | Scaled | Allocated |
------------------------------------- |--------------:|-----------:|-----------:|-------:|----------:|
                                 Sync |   599.9187 ns |  2.1857 ns | 1666892.40 |   1.00 |      0 kB |
 'ValueTask + Local Async via Params' |   668.2087 ns |  4.8867 ns | 1496538.39 |   1.11 |      0 kB |
 'ValueTask + Local Async via Locals' | 1,525.9064 ns |  9.9561 ns |  655348.19 |   2.54 |   2.45 kB |
               'ValueTask Pure Async' | 5,606.6241 ns | 18.2806 ns |  178360.45 |   9.35 |      0 kB |

The ValueTask path is pretty close to the sync path when passing to the local function async fallback via params; but not when using locals; even though the fallback is never used.

/cc @mgravell @stephentoub @davidfowl

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions