BenchmarkDotNet icon indicating copy to clipboard operation
BenchmarkDotNet copied to clipboard

Support custom awaitable types

Open timcassell opened this issue 2 years ago • 0 comments
trafficstars

This is a prototype for supporting any type of awaitable, stemmed from the discussion in #2111. (It might even be able to supersede that PR, since I am seeing even better Task benchmark results).

New interfaces are used for BDN to know how to make a type awaitable.

public interface IAwaitableAdapter<TAwaitable, TAwaiter>
    where TAwaiter : ICriticalNotifyCompletion
{
    public TAwaiter GetAwaiter(ref TAwaitable awaitable);
    public bool GetIsCompleted(ref TAwaiter awaiter);
    public void GetResult(ref TAwaiter awaiter);
}

public interface IAwaitableAdapter<TAwaitable, TAwaiter, TResult>
    where TAwaiter : ICriticalNotifyCompletion
{
    public TAwaiter GetAwaiter(ref TAwaitable awaitable);
    public bool GetIsCompleted(ref TAwaiter awaiter);
    public TResult GetResult(ref TAwaiter awaiter);
}

An adapter type is added to the config to make the benchmark await it, along with an optional async method builder adapter (more on that below).

Example

[Config(typeof(Config))]
[MemoryDiagnoser(false)]
public class Benchmarks
{
    private class Config : ManualConfig
    {
        public Config()
        {
            AddAsyncAdapter(typeof(YieldAwaitableAdapter));
            AddAsyncAdapter(typeof(PromiseAdapter), typeof(PromiseMethodBuilderAdapter));
            AddAsyncAdapter(typeof(PromiseAdapter<>), typeof(PromiseMethodBuilderAdapter));
            AddJob(Job.Default);
        }
    }

    [Benchmark]
    public YieldAwaitable PureYield() => Task.Yield();

    [Benchmark]
    public async Promise PromiseVoid()
    {
        await Task.Yield();
    }

    [Benchmark]
    public async Promise<long> PromiseLong()
    {
        await Task.Yield();
        return default;
    }

    [Benchmark]
    public async Task TaskVoid()
    {
        await Task.Yield();
    }

    [Benchmark]
    public async Task<long> TaskLong()
    {
        await Task.Yield();
        return default;
    }
}

public struct YieldAwaitableAdapter : IAwaitableAdapter<YieldAwaitable, YieldAwaitable.YieldAwaiter>
{
    public YieldAwaitable.YieldAwaiter GetAwaiter(ref YieldAwaitable awaitable) => awaitable.GetAwaiter();
    public bool GetIsCompleted(ref YieldAwaitable.YieldAwaiter awaiter) => awaiter.IsCompleted;
    public void GetResult(ref YieldAwaitable.YieldAwaiter awaiter) => awaiter.GetResult();
}

public struct PromiseAdapter : IAwaitableAdapter<Promise, PromiseAwaiterVoid>
{
    public PromiseAwaiterVoid GetAwaiter(ref Promise awaitable) => awaitable.GetAwaiter();
    public bool GetIsCompleted(ref PromiseAwaiterVoid awaiter) => awaiter.IsCompleted;
    public void GetResult(ref PromiseAwaiterVoid awaiter) => awaiter.GetResult();
}

public struct PromiseAdapter<T> : IAwaitableAdapter<Promise<T>, PromiseAwaiter<T>, T>
{
    public PromiseAwaiter<T> GetAwaiter(ref Promise<T> awaitable) => awaitable.GetAwaiter();
    public bool GetIsCompleted(ref PromiseAwaiter<T> awaiter) => awaiter.IsCompleted;
    public T GetResult(ref PromiseAwaiter<T> awaiter) => awaiter.GetResult();
}

public struct PromiseMethodBuilderAdapter : IAsyncMethodBuilderAdapter
{
    private struct EmptyStruct { }

    private PromiseMethodBuilder<EmptyStruct> _builder;

    public void CreateAsyncMethodBuilder()
        => _builder = PromiseMethodBuilder<EmptyStruct>.Create();

    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
        => _builder.Start(ref stateMachine);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
        => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);

    public void SetResult()
    {
        _builder.SetResult(default);
        _builder.Task.Forget();
    }

    public void SetStateMachine(IAsyncStateMachine stateMachine)
        => _builder.SetStateMachine(stateMachine);
}

And the results:

Method Mean Error StdDev Allocated
PureYield 669.9 ns 13.33 ns 19.54 ns -
PromiseVoid 1,215.9 ns 24.16 ns 28.76 ns 32 B
PromiseLong 1,161.9 ns 18.90 ns 17.68 ns 32 B
TaskVoid 1,396.2 ns 6.82 ns 6.38 ns 112 B
TaskLong 1,443.0 ns 23.52 ns 22.00 ns 112 B

Woah, we can actually measure the cost of await Task.Yield() now, which was previously impossible!

IAsyncMethodBuilderAdapter is an optional adapter type used to override the default AsyncTaskMethodBuilder, because some builders use more efficient await strategies for known awaiter types. For example, I measured ~100ns slowdown with Promise when using the default AsyncTaskMethodBuilder instead of using the custom PromiseMethodBuilder.

Currently in this prototype, I only supported custom awaits with the config, no attribute support. If we want attribute support, I'm not sure how it should look like. Also, I haven't updated the InProcessEmitToolchain with this system yet, because I want some feedback first.

Also, I did test this with the InProcessNoEmitToolchain with NativeAOT, and it worked flawlessly.

cc @YegorStepanov

timcassell avatar Jul 02 '23 08:07 timcassell