Skip to content

Commit

Permalink
Optimize for Task, ValueTask cases
Browse files Browse the repository at this point in the history
  • Loading branch information
heku committed Jun 12, 2024
1 parent f029655 commit f0ff6eb
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 39 deletions.
16 changes: 8 additions & 8 deletions Kunet.AsyncInterceptor.PerfTests/AsyncInterceptorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,18 @@ public ValueTask<int> KunetRunningValueTaskT()
//| KunetRunningValueTaskT | ShortRun-.NET 8.0 | .NET 8.0 | RunningValueTask<T> | 436.08 ns | 0.0725 | 1216 B |
//| StakxRunningValueTaskT | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | RunningValueTask<T> | 7,132.29 ns | 0.5951 | 3755 B |
//| KunetRunningValueTaskT | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | RunningValueTask<T> | 1,564.72 ns | 0.3147 | 1982 B |
//| StakxTask | ShortRun-.NET 8.0 | .NET 8.0 | Task | 940.71 ns | 0.0448 | 752 B |
//| KunetTask | ShortRun-.NET 8.0 | .NET 8.0 | Task | 77.63 ns | 0.0196 | 328 B |
//| StakxTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | Task | 3,320.81 ns | 0.1793 | 1140 B |
//| KunetTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | Task | 166.57 ns | 0.0560 | 353 B |
//| StakxTask | ShortRun-.NET 8.0 | .NET 8.0 | Task | 901.41 ns | 0.0448 | 752 B |
//| KunetTask | ShortRun-.NET 8.0 | .NET 8.0 | Task | 57.36 ns | 0.0157 | 264 B |
//| StakxTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | Task | 3,259.66 ns | 0.1793 | 1140 B |
//| KunetTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | Task | 112.87 ns | 0.0459 | 289 B |
//| StakxTaskT | ShortRun-.NET 8.0 | .NET 8.0 | Task<T> | 1,352.91 ns | 0.0668 | 1144 B |
//| KunetTaskT | ShortRun-.NET 8.0 | .NET 8.0 | Task<T> | 168.32 ns | 0.0348 | 584 B |
//| StakxTaskT | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | Task<T> | 4,475.84 ns | 0.3281 | 2071 B |
//| KunetTaskT | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | Task<T> | 601.14 ns | 0.1516 | 955 B |
//| StakxValueTask | ShortRun-.NET 8.0 | .NET 8.0 | ValueTask | 1,832.72 ns | 0.0629 | 1080 B |
//| KunetValueTask | ShortRun-.NET 8.0 | .NET 8.0 | ValueTask | 86.66 ns | 0.0234 | 392 B |
//| StakxValueTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | ValueTask | 3,652.26 ns | 0.2022 | 1276 B |
//| KunetValueTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | ValueTask | 162.22 ns | 0.0675 | 425 B |
//| StakxValueTask | ShortRun-.NET 8.0 | .NET 8.0 | ValueTask | 1,740.57 ns | 0.0629 | 1080 B |
//| KunetValueTask | ShortRun-.NET 8.0 | .NET 8.0 | ValueTask | 60.98 ns | 0.0196 | 328 B |
//| StakxValueTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | ValueTask | 3,711.97 ns | 0.2022 | 1276 B |
//| KunetValueTask | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | ValueTask | 124.07 ns | 0.0572 | 361 B |
//| StakxValueTaskT | ShortRun-.NET 8.0 | .NET 8.0 | ValueTask<T> | 2,110.97 ns | 0.0877 | 1472 B |
//| KunetValueTaskT | ShortRun-.NET 8.0 | .NET 8.0 | ValueTask<T> | 187.48 ns | 0.0391 | 656 B |
//| StakxValueTaskT | ShortRun-.NET Framework 4.8.1 | .NET Framework 4.8.1 | ValueTask<T> | 4,636.58 ns | 0.3204 | 2055 B |
Expand Down
2 changes: 0 additions & 2 deletions Kunet.AsyncInterceptor/AsyncAdapter.Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ public partial class AsyncAdapter

static AsyncAdapter()
{
Register<Task>(x => new AsyncAdapterOfTask(x)); // Task
Register<ValueTask>(x => new AsyncAdapterOfValueTask(x)); // ValueTask
Register(typeof(Task<>), typeof(AsyncAdapterOfTask<>)); // Task<T>
Register(typeof(ValueTask<>), typeof(AsyncAdapterOfValueTask<>)); // ValueTask<T>
}
Expand Down
40 changes: 32 additions & 8 deletions Kunet.AsyncInterceptor/AsyncAdapters/AsyncAdapterOfTask.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
using Castle.DynamicProxy;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace Kunet.AsyncInterceptor;

internal sealed class AsyncAdapterOfTask : AsyncAdapter
[StructLayout(LayoutKind.Auto)]
internal struct AsyncAdapterOfTask : IAsyncInvocation, IAsyncAdapter
{
private readonly IInvocationProceedInfo _proceed;
private readonly AsyncTaskMethodBuilder _builder = AsyncTaskMethodBuilder.Create();

public AsyncAdapterOfTask(IInvocation invocation) : base(invocation) => Task = _builder.Task;
public AsyncAdapterOfTask(IInvocation invocation)
{
_proceed = invocation.CaptureProceedInfo();
Invocation = invocation;
Task = _builder.Task;
}

protected override ValueTask SetAsyncResult() => new((Task)Invocation.ReturnValue);
public IInvocation Invocation { get; }

public override object Task { get; }
public object AsyncResult { get; set; }

public override void Start<TStateMachine>(ref TStateMachine stateMachine) => _builder.Start(ref stateMachine);
public object Task { get; }

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

public override void SetException(Exception exception) => _builder.SetException(exception);
public ValueTask ProceedAsync()
{
_proceed.Invoke(); // Invocation.ReturnValue = NEXT()
if (Invocation.ReturnValue is not null)
{
Debug.Assert(Invocation.ReturnValue is Task);
return new((Task)Invocation.ReturnValue); // AsyncResult = await Invocation.ReturnValue
}
return default;
}

public override void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
public void SetException(Exception exception) => _builder.SetException(exception);

public void SetResult(object result) => _builder.SetResult();

public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => _builder.Start(ref stateMachine);
}
40 changes: 32 additions & 8 deletions Kunet.AsyncInterceptor/AsyncAdapters/AsyncAdapterOfValueTask.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
using Castle.DynamicProxy;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace Kunet.AsyncInterceptor;

internal sealed class AsyncAdapterOfValueTask : AsyncAdapter
[StructLayout(LayoutKind.Auto)]
internal struct AsyncAdapterOfValueTask : IAsyncInvocation, IAsyncAdapter
{
private readonly IInvocationProceedInfo _proceed;
private readonly AsyncValueTaskMethodBuilder _builder = AsyncValueTaskMethodBuilder.Create();

public AsyncAdapterOfValueTask(IInvocation invocation) : base(invocation) => Task = _builder.Task;
public AsyncAdapterOfValueTask(IInvocation invocation)
{
_proceed = invocation.CaptureProceedInfo();
Invocation = invocation;
Task = _builder.Task;
}

protected override ValueTask SetAsyncResult() => (ValueTask)Invocation.ReturnValue;
public IInvocation Invocation { get; }

public override object Task { get; }
public object AsyncResult { get; set; }

public override void Start<TStateMachine>(ref TStateMachine stateMachine) => _builder.Start(ref stateMachine);
public object Task { get; }

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

public override void SetException(Exception exception) => _builder.SetException(exception);
public ValueTask ProceedAsync()
{
_proceed.Invoke(); // Invocation.ReturnValue = NEXT()
if (Invocation.ReturnValue is not null)
{
Debug.Assert(Invocation.ReturnValue is ValueTask);
return (ValueTask)Invocation.ReturnValue; // AsyncResult = await Invocation.ReturnValue
}
return default;
}

public override void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) => _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
public void SetException(Exception exception) => _builder.SetException(exception);

public void SetResult(object result) => _builder.SetResult();

public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => _builder.Start(ref stateMachine);
}
26 changes: 24 additions & 2 deletions Kunet.AsyncInterceptor/AsyncInterceptor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Castle.DynamicProxy;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace Kunet.AsyncInterceptor;
Expand All @@ -10,9 +11,26 @@ public abstract class AsyncInterceptor : IInterceptor
{
void IInterceptor.Intercept(IInvocation invocation)
{
if (AsyncAdapter.TryCreate(invocation, out var adapter))
var returnType = invocation.Method.ReturnType;
if (returnType == typeof(Task))
{
AsyncStateMachine stateMachine = new(adapter, InterceptAsync);
var adapter = new AsyncAdapterOfTask(invocation);
var stateMachine = new AsyncStateMachine<AsyncAdapterOfTask>(in adapter, this);
adapter.Start(ref stateMachine);
Debug.Assert(adapter.Task is not null);
invocation.ReturnValue = adapter.Task;
}
else if (returnType == typeof(ValueTask))
{
var adapter = new AsyncAdapterOfValueTask(invocation);
var stateMachine = new AsyncStateMachine<AsyncAdapterOfValueTask>(in adapter, this);
adapter.Start(ref stateMachine);
Debug.Assert(adapter.Task is not null);
invocation.ReturnValue = adapter.Task;
}
else if (AsyncAdapter.TryCreate(invocation, out var adapter))
{
AsyncStateMachine stateMachine = new(adapter, this);
adapter.Start(ref stateMachine);
Debug.Assert(adapter.Task is not null);
invocation.ReturnValue = adapter.Task;
Expand All @@ -26,4 +44,8 @@ void IInterceptor.Intercept(IInvocation invocation)
protected abstract void Intercept(IInvocation invocation);

protected abstract ValueTask InterceptAsync(IAsyncInvocation invocation);

// Compat.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ValueTask InternalInterceptAsync(IAsyncInvocation invocation) => InterceptAsync(invocation);
}
27 changes: 16 additions & 11 deletions Kunet.AsyncInterceptor/AsyncStateMachine.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace Kunet.AsyncInterceptor;

[StructLayout(LayoutKind.Auto)]
internal struct AsyncStateMachine : IAsyncStateMachine
{
private readonly AsyncAdapter _adapter;
private readonly Func<IAsyncInvocation, ValueTask> _interceptAsync;
private ValueTask? _interceptingTask;
private readonly AsyncInterceptor _interceptor;
private bool _intercepting;
private ValueTaskAwaiter _interceptingAwaiter;

public AsyncStateMachine(AsyncAdapter adapter, Func<IAsyncInvocation, ValueTask> interceptAsync)
public AsyncStateMachine(AsyncAdapter adapter, AsyncInterceptor interceptor)
{
_adapter = adapter;
_interceptAsync = interceptAsync;
_interceptingTask = null;
_interceptor = interceptor;
}

public void MoveNext()
{
try
{
_interceptingTask ??= _interceptAsync(_adapter);
var awaiter = _interceptingTask.Value.GetAwaiter();
if (awaiter.IsCompleted)
if (_intercepting is false)
{
awaiter.GetResult(); // throw exception if there is.
_intercepting = true;
_interceptingAwaiter = _interceptor.InternalInterceptAsync(_adapter).GetAwaiter();
}
if (_interceptingAwaiter.IsCompleted)
{
_interceptingAwaiter.GetResult(); // throw exception if there is.
_adapter.SetResult(_adapter.AsyncResult);
}
else
{
_adapter.AwaitUnsafeOnCompleted(ref awaiter, ref this);
_adapter.AwaitUnsafeOnCompleted(ref _interceptingAwaiter, ref this);
}
}
catch (Exception ex)
Expand All @@ -39,6 +43,7 @@ public void MoveNext()
}
}

[Obsolete]
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
// SetStateMachine was originally needed in order to store the boxed state machine reference into the boxed copy.
Expand Down
53 changes: 53 additions & 0 deletions Kunet.AsyncInterceptor/AsyncStateMachine`.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Kunet.AsyncInterceptor;

[StructLayout(LayoutKind.Auto)]
internal struct AsyncStateMachine<T> : IAsyncStateMachine where T : IAsyncAdapter
{
private readonly T _adapter;
private readonly AsyncInterceptor _interceptor;
private bool _intercepting;
private ValueTaskAwaiter _interceptingAwaiter;

public AsyncStateMachine(in T adapter, AsyncInterceptor interceptor)
{
_adapter = adapter;
_interceptor = interceptor;
}

public void MoveNext()
{
try
{
if (_intercepting is false)
{
_intercepting = true;
_interceptingAwaiter = _interceptor.InternalInterceptAsync(_adapter).GetAwaiter();
}
if (_interceptingAwaiter.IsCompleted)
{
_interceptingAwaiter.GetResult(); // throw exception if there is.
_adapter.SetResult(_adapter.AsyncResult);
}
else
{
_adapter.AwaitUnsafeOnCompleted(ref _interceptingAwaiter, ref this);
}
}
catch (Exception ex)
{
_adapter.SetException(ex);
}
}

[Obsolete]
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
// SetStateMachine was originally needed in order to store the boxed state machine reference into the boxed copy.
// Now that a normal box is no longer used, SetStateMachine is also legacy. We need not do anything here.
// https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs,70528b49b7e9916f
}
}

0 comments on commit f0ff6eb

Please sign in to comment.