Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom awaitable types #2349

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

timcassell
Copy link
Collaborator

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

Force async unroll factor to 1.
Support async IterationSetup/IterationCleanup.
# Conflicts:
#	tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs
…ead-new

# Conflicts:
#	tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs
# Conflicts:
#	src/BenchmarkDotNet/Engines/IEngine.cs
#	src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/ConsumableTypeInfo.cs
#	src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs
# Conflicts:
#	src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkAction.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/BenchmarkActionFactory_Implementations.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/TaskConsumeEmitter.cs
#	src/BenchmarkDotNet/Toolchains/InProcess/InProcessRunner.cs
#	tests/BenchmarkDotNet.IntegrationTests/InProcessTest.cs
Use async consumers in toolchains (WIP).
Handle overhead without duplicating the state machine code.
Split AsyncBenchmarkRunner workload and overhead.
Use abstract base class without generics.
Explicitly specify overhead awaitable/awaiter types.
Use awaitable types in InProcessNoEmitToolchain.
Support multiple generic arguments in adapters.
No need to pass awaitable type to config, only pass awaitable adapter type.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant